@bouncer-protocol/bouncer 0.3.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 +65 -0
- package/dist/index.d.ts +209 -0
- package/dist/index.js +836 -0
- package/dist/index.js.map +1 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# @openclaw/bouncer
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin for the Bouncer Protocol. Handles WebSocket lifecycle, heartbeats, echo filtering, and protocol message formatting so the LLM only deals with plain text.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
The plugin registers as an OpenClaw channel. Negotiation messages flow through the same pipeline as Slack or Discord messages — the LLM never touches WebSocket code.
|
|
8
|
+
|
|
9
|
+
**Bouncer role (receives offers):**
|
|
10
|
+
- Plugin connects to the lobby WebSocket on startup
|
|
11
|
+
- Auto-accepts incoming negotiation requests
|
|
12
|
+
- Routes messages from the other agent to the LLM
|
|
13
|
+
- Parses the LLM's plain text response into protocol messages (`[ACCEPT]`, `[WALK]`, `[TERMS]{...}`, etc.)
|
|
14
|
+
|
|
15
|
+
**Brand role (sends offers):**
|
|
16
|
+
- Registers tools: `bouncer_browse`, `bouncer_profile`, `bouncer_offer`
|
|
17
|
+
- The LLM uses these tools to find a bouncer and submit an offer
|
|
18
|
+
- Once accepted, the plugin connects to the negotiation room and routes messages
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# From the bouncer-protocol monorepo
|
|
24
|
+
cd apps/openclaw-plugin
|
|
25
|
+
pnpm build
|
|
26
|
+
|
|
27
|
+
# Install into OpenClaw (local link)
|
|
28
|
+
openclaw plugins install -l /path/to/bouncer-protocol/apps/openclaw-plugin
|
|
29
|
+
openclaw plugins enable bouncer
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Add to `~/.openclaw/openclaw.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"channels": {
|
|
39
|
+
"bouncer": {
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"role": "bouncer",
|
|
42
|
+
"apiUrl": "https://api.bouncer.cash",
|
|
43
|
+
"apiKey": "bsk_your_jwt_here",
|
|
44
|
+
"agentId": "your-agent-uuid",
|
|
45
|
+
"autoAccept": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"plugins": {
|
|
49
|
+
"entries": {
|
|
50
|
+
"bouncer": { "enabled": true }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"bindings": [
|
|
54
|
+
{ "agentId": "bob", "match": { "channel": "bouncer" } }
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For brand agents, set `"role": "brand"` and use a `brk_` API key.
|
|
60
|
+
|
|
61
|
+
## What This Solves
|
|
62
|
+
|
|
63
|
+
Without the plugin, LLMs must manage their own WebSocket connections — writing scripts, handling heartbeats, filtering echoes, formatting JSON. This fails on smaller models.
|
|
64
|
+
|
|
65
|
+
With the plugin, the LLM just writes text like "I can offer 15% off plus free shipping" and the plugin handles everything else. Works with any model.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type stubs for the OpenClaw Plugin SDK.
|
|
3
|
+
*
|
|
4
|
+
* When installed into an OpenClaw environment, replace these with:
|
|
5
|
+
* import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
6
|
+
*
|
|
7
|
+
* These stubs match the contract from openclaw/openclaw src/plugin-sdk/core.ts
|
|
8
|
+
* and src/plugins/types.ts as of 2026-03.
|
|
9
|
+
*/
|
|
10
|
+
interface PluginLogger {
|
|
11
|
+
info(msg: string, ...args: unknown[]): void;
|
|
12
|
+
warn(msg: string, ...args: unknown[]): void;
|
|
13
|
+
error(msg: string, ...args: unknown[]): void;
|
|
14
|
+
debug(msg: string, ...args: unknown[]): void;
|
|
15
|
+
}
|
|
16
|
+
interface ReplyPayload {
|
|
17
|
+
text?: string;
|
|
18
|
+
body?: string;
|
|
19
|
+
mediaUrl?: string;
|
|
20
|
+
mediaUrls?: string[];
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
interface MsgContext {
|
|
24
|
+
Body?: string;
|
|
25
|
+
BodyForAgent?: string;
|
|
26
|
+
RawBody?: string;
|
|
27
|
+
CommandBody?: string;
|
|
28
|
+
From?: string;
|
|
29
|
+
To?: string;
|
|
30
|
+
SessionKey?: string;
|
|
31
|
+
AccountId?: string;
|
|
32
|
+
OriginatingChannel?: string;
|
|
33
|
+
OriginatingTo?: string;
|
|
34
|
+
ChatType?: string;
|
|
35
|
+
SenderName?: string;
|
|
36
|
+
SenderId?: string;
|
|
37
|
+
Provider?: string;
|
|
38
|
+
Surface?: string;
|
|
39
|
+
ConversationLabel?: string;
|
|
40
|
+
Timestamp?: number;
|
|
41
|
+
CommandAuthorized?: boolean;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
type FinalizedMsgContext = Omit<MsgContext, "CommandAuthorized"> & {
|
|
45
|
+
CommandAuthorized: boolean;
|
|
46
|
+
};
|
|
47
|
+
interface ReplyDispatcherWithTypingOptions {
|
|
48
|
+
deliver: (payload: ReplyPayload, info: {
|
|
49
|
+
kind: string;
|
|
50
|
+
}) => Promise<void>;
|
|
51
|
+
onReplyStart?: () => Promise<void> | void;
|
|
52
|
+
onIdle?: () => void;
|
|
53
|
+
onCleanup?: () => void;
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
interface DispatchInboundResult {
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
interface PluginRuntime {
|
|
60
|
+
channel: {
|
|
61
|
+
reply: {
|
|
62
|
+
finalizeInboundContext: <T extends Record<string, unknown>>(ctx: T) => T & FinalizedMsgContext;
|
|
63
|
+
dispatchReplyWithBufferedBlockDispatcher: (params: {
|
|
64
|
+
ctx: MsgContext | FinalizedMsgContext;
|
|
65
|
+
cfg: OpenClawConfig;
|
|
66
|
+
dispatcherOptions: ReplyDispatcherWithTypingOptions;
|
|
67
|
+
replyOptions?: Record<string, unknown>;
|
|
68
|
+
}) => Promise<DispatchInboundResult>;
|
|
69
|
+
dispatchReplyFromConfig?: (...args: unknown[]) => Promise<unknown>;
|
|
70
|
+
};
|
|
71
|
+
debounce: {
|
|
72
|
+
createInboundDebouncer: (...args: unknown[]) => unknown;
|
|
73
|
+
resolveInboundDebounceMs: (...args: unknown[]) => number;
|
|
74
|
+
};
|
|
75
|
+
session: {
|
|
76
|
+
recordInboundSession: (...args: unknown[]) => unknown;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
};
|
|
79
|
+
[key: string]: unknown;
|
|
80
|
+
};
|
|
81
|
+
config: {
|
|
82
|
+
loadConfig: () => Promise<OpenClawConfig>;
|
|
83
|
+
writeConfigFile?: (...args: unknown[]) => Promise<void>;
|
|
84
|
+
};
|
|
85
|
+
logging: {
|
|
86
|
+
shouldLogVerbose(): boolean;
|
|
87
|
+
getChildLogger(name: string): PluginLogger;
|
|
88
|
+
};
|
|
89
|
+
state: {
|
|
90
|
+
resolveStateDir(): string;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
interface OpenClawConfig {
|
|
94
|
+
channels?: Record<string, Record<string, unknown>>;
|
|
95
|
+
plugins?: Record<string, unknown>;
|
|
96
|
+
[key: string]: unknown;
|
|
97
|
+
}
|
|
98
|
+
interface OpenClawPluginServiceContext {
|
|
99
|
+
config: OpenClawConfig;
|
|
100
|
+
workspaceDir?: string;
|
|
101
|
+
stateDir: string;
|
|
102
|
+
logger: PluginLogger;
|
|
103
|
+
}
|
|
104
|
+
interface OpenClawPluginService {
|
|
105
|
+
id: string;
|
|
106
|
+
start: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
|
|
107
|
+
stop?: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
interface OpenClawPluginToolContext {
|
|
110
|
+
config?: OpenClawConfig;
|
|
111
|
+
agentId?: string;
|
|
112
|
+
sessionKey?: string;
|
|
113
|
+
messageChannel?: string;
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
}
|
|
116
|
+
interface AgentTool {
|
|
117
|
+
name: string;
|
|
118
|
+
description: string;
|
|
119
|
+
parameters?: Record<string, unknown>;
|
|
120
|
+
execute: (params: Record<string, unknown>, ctx?: unknown) => Promise<unknown>;
|
|
121
|
+
}
|
|
122
|
+
type OpenClawPluginToolFactory = (ctx: OpenClawPluginToolContext) => AgentTool | AgentTool[] | null | undefined;
|
|
123
|
+
interface OpenClawPluginApi {
|
|
124
|
+
id: string;
|
|
125
|
+
name: string;
|
|
126
|
+
version?: string;
|
|
127
|
+
description?: string;
|
|
128
|
+
source: string;
|
|
129
|
+
rootDir?: string;
|
|
130
|
+
registrationMode: "full" | "setup-only" | "setup-runtime";
|
|
131
|
+
config: OpenClawConfig;
|
|
132
|
+
pluginConfig?: Record<string, unknown>;
|
|
133
|
+
runtime: PluginRuntime;
|
|
134
|
+
logger: PluginLogger;
|
|
135
|
+
registerTool: (tool: AgentTool | OpenClawPluginToolFactory, opts?: {
|
|
136
|
+
name?: string;
|
|
137
|
+
names?: string[];
|
|
138
|
+
optional?: boolean;
|
|
139
|
+
}) => void;
|
|
140
|
+
registerHook: (events: string | string[], handler: (...args: unknown[]) => void | Promise<void>, opts?: Record<string, unknown>) => void;
|
|
141
|
+
registerHttpRoute: (params: Record<string, unknown>) => void;
|
|
142
|
+
registerChannel: (registration: {
|
|
143
|
+
plugin: ChannelPlugin;
|
|
144
|
+
} | ChannelPlugin) => void;
|
|
145
|
+
registerGatewayMethod: (method: string, handler: (...args: unknown[]) => unknown) => void;
|
|
146
|
+
registerService: (service: OpenClawPluginService) => void;
|
|
147
|
+
registerCommand: (command: {
|
|
148
|
+
name: string;
|
|
149
|
+
description: string;
|
|
150
|
+
handler: (...args: unknown[]) => void | Promise<void>;
|
|
151
|
+
}) => void;
|
|
152
|
+
resolvePath: (input: string) => string;
|
|
153
|
+
on: (hookName: string, handler: (...args: unknown[]) => void | Promise<void>, opts?: {
|
|
154
|
+
priority?: number;
|
|
155
|
+
}) => void;
|
|
156
|
+
}
|
|
157
|
+
interface ChannelMeta {
|
|
158
|
+
id: string;
|
|
159
|
+
label: string;
|
|
160
|
+
selectionLabel: string;
|
|
161
|
+
docsPath?: string;
|
|
162
|
+
blurb?: string;
|
|
163
|
+
aliases?: string[];
|
|
164
|
+
order?: number;
|
|
165
|
+
}
|
|
166
|
+
interface ChannelCapabilities {
|
|
167
|
+
chatTypes: Array<"direct" | "group">;
|
|
168
|
+
}
|
|
169
|
+
interface ChannelConfigAdapter {
|
|
170
|
+
listAccountIds: (cfg: OpenClawConfig) => string[];
|
|
171
|
+
resolveAccount: (cfg: OpenClawConfig, accountId?: string) => Record<string, unknown>;
|
|
172
|
+
}
|
|
173
|
+
interface ChannelOutboundAdapter {
|
|
174
|
+
deliveryMode: "direct" | "queued";
|
|
175
|
+
sendText: (ctx: SendTextContext) => Promise<{
|
|
176
|
+
ok: boolean;
|
|
177
|
+
error?: string;
|
|
178
|
+
}>;
|
|
179
|
+
}
|
|
180
|
+
interface SendTextContext {
|
|
181
|
+
text: string;
|
|
182
|
+
conversationId?: string;
|
|
183
|
+
accountId?: string;
|
|
184
|
+
replyTo?: string;
|
|
185
|
+
[key: string]: unknown;
|
|
186
|
+
}
|
|
187
|
+
interface ChannelLifecycleAdapter {
|
|
188
|
+
startAccount?: (accountId: string, config: Record<string, unknown>, logger: PluginLogger) => Promise<void>;
|
|
189
|
+
stopAccount?: (accountId: string) => Promise<void>;
|
|
190
|
+
}
|
|
191
|
+
interface ChannelPlugin {
|
|
192
|
+
id: string;
|
|
193
|
+
meta: ChannelMeta;
|
|
194
|
+
capabilities: ChannelCapabilities;
|
|
195
|
+
config: ChannelConfigAdapter;
|
|
196
|
+
outbound?: ChannelOutboundAdapter;
|
|
197
|
+
lifecycle?: ChannelLifecycleAdapter;
|
|
198
|
+
[key: string]: unknown;
|
|
199
|
+
}
|
|
200
|
+
interface DefinedPluginEntry {
|
|
201
|
+
id: string;
|
|
202
|
+
name: string;
|
|
203
|
+
description: string;
|
|
204
|
+
register: (api: OpenClawPluginApi) => void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
declare const _default: DefinedPluginEntry;
|
|
208
|
+
|
|
209
|
+
export { _default as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
// src/sdk-types.ts
|
|
2
|
+
function definePluginEntry(opts) {
|
|
3
|
+
return {
|
|
4
|
+
id: opts.id,
|
|
5
|
+
name: opts.name,
|
|
6
|
+
description: opts.description,
|
|
7
|
+
register: opts.register
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/connector-parser.ts
|
|
12
|
+
var SIMPLE_PREFIXES = {
|
|
13
|
+
"[ACCEPT]": "accept",
|
|
14
|
+
"[WALK]": "walk",
|
|
15
|
+
"[RESEARCH]": "research"
|
|
16
|
+
};
|
|
17
|
+
var TERMS_PREFIXES = {
|
|
18
|
+
"[TERMS]": "propose_terms",
|
|
19
|
+
"[COUNTER]": "counter_terms"
|
|
20
|
+
};
|
|
21
|
+
function parseResponse(text) {
|
|
22
|
+
const trimmed = text.trim();
|
|
23
|
+
for (const [prefix, type] of Object.entries(SIMPLE_PREFIXES)) {
|
|
24
|
+
if (trimmed.startsWith(prefix)) {
|
|
25
|
+
return { type, content: trimmed.slice(prefix.length).trim() };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const [prefix, type] of Object.entries(TERMS_PREFIXES)) {
|
|
29
|
+
if (trimmed.startsWith(prefix)) {
|
|
30
|
+
const rest = trimmed.slice(prefix.length);
|
|
31
|
+
const jsonEnd = rest.indexOf("}");
|
|
32
|
+
if (jsonEnd !== -1) {
|
|
33
|
+
const jsonStr = rest.slice(0, jsonEnd + 1);
|
|
34
|
+
try {
|
|
35
|
+
const terms = JSON.parse(jsonStr);
|
|
36
|
+
const content = rest.slice(jsonEnd + 1).trim();
|
|
37
|
+
return { type, content, terms };
|
|
38
|
+
} catch {
|
|
39
|
+
return { type: "message", content: trimmed };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { type: "message", content: trimmed };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { type: "message", content: trimmed };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/bouncer-channel.ts
|
|
49
|
+
var activeSessions = /* @__PURE__ */ new Map();
|
|
50
|
+
function setActiveSession(negotiationId, session) {
|
|
51
|
+
activeSessions.set(negotiationId, session);
|
|
52
|
+
}
|
|
53
|
+
function removeActiveSession(negotiationId) {
|
|
54
|
+
const session = activeSessions.get(negotiationId);
|
|
55
|
+
if (session) {
|
|
56
|
+
session.close();
|
|
57
|
+
activeSessions.delete(negotiationId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function getActiveNegotiationId() {
|
|
61
|
+
const entries = [...activeSessions.entries()];
|
|
62
|
+
return entries.length > 0 ? entries[0][0] : void 0;
|
|
63
|
+
}
|
|
64
|
+
function createBouncerChannel(logger) {
|
|
65
|
+
return {
|
|
66
|
+
id: "bouncer",
|
|
67
|
+
meta: {
|
|
68
|
+
id: "bouncer",
|
|
69
|
+
label: "Bouncer Protocol",
|
|
70
|
+
selectionLabel: "Bouncer Protocol (Agent Negotiation)",
|
|
71
|
+
docsPath: "/channels/bouncer",
|
|
72
|
+
blurb: "Real-time agent-to-agent negotiation via the Bouncer Protocol",
|
|
73
|
+
aliases: ["bouncer-protocol"],
|
|
74
|
+
order: 90
|
|
75
|
+
},
|
|
76
|
+
capabilities: {
|
|
77
|
+
chatTypes: ["direct"]
|
|
78
|
+
},
|
|
79
|
+
config: {
|
|
80
|
+
listAccountIds(cfg) {
|
|
81
|
+
const bouncer = cfg.channels?.bouncer;
|
|
82
|
+
if (!bouncer?.agentId) return [];
|
|
83
|
+
return [bouncer.agentId];
|
|
84
|
+
},
|
|
85
|
+
resolveAccount(cfg, accountId) {
|
|
86
|
+
const bouncer = cfg.channels?.bouncer;
|
|
87
|
+
return bouncer ?? { accountId };
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
outbound: {
|
|
91
|
+
deliveryMode: "direct",
|
|
92
|
+
async sendText(ctx) {
|
|
93
|
+
const negId = ctx.conversationId ?? getActiveNegotiationId();
|
|
94
|
+
if (!negId) {
|
|
95
|
+
return { ok: false, error: "No active negotiation session" };
|
|
96
|
+
}
|
|
97
|
+
const session = activeSessions.get(negId);
|
|
98
|
+
if (!session) {
|
|
99
|
+
return { ok: false, error: `No session for negotiation ${negId}` };
|
|
100
|
+
}
|
|
101
|
+
const parsed = parseResponse(ctx.text);
|
|
102
|
+
session.sendThinking();
|
|
103
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
104
|
+
session.send(parsed);
|
|
105
|
+
logger.info(`[bouncer] Sent ${parsed.type}: ${parsed.content.slice(0, 80)}...`);
|
|
106
|
+
return { ok: true };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/connector-lobby.ts
|
|
113
|
+
import { EventEmitter } from "events";
|
|
114
|
+
import WebSocket from "ws";
|
|
115
|
+
var requestCounter = 0;
|
|
116
|
+
var LobbyClient = class extends EventEmitter {
|
|
117
|
+
ws = null;
|
|
118
|
+
opts;
|
|
119
|
+
reconnectAttempt = 0;
|
|
120
|
+
reconnectTimer = null;
|
|
121
|
+
closed = false;
|
|
122
|
+
constructor(opts) {
|
|
123
|
+
super();
|
|
124
|
+
this.opts = {
|
|
125
|
+
reconnectBaseMs: 1e3,
|
|
126
|
+
reconnectMaxMs: 3e4,
|
|
127
|
+
...opts
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
connect() {
|
|
131
|
+
this.closed = false;
|
|
132
|
+
return this._connect();
|
|
133
|
+
}
|
|
134
|
+
_connect() {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const url = `${this.opts.apiUrl}/v1/agents/${this.opts.agentId}/ws?token=${this.opts.token}`;
|
|
137
|
+
const ws = new WebSocket(url);
|
|
138
|
+
this.ws = ws;
|
|
139
|
+
ws.on("open", () => {
|
|
140
|
+
this.reconnectAttempt = 0;
|
|
141
|
+
});
|
|
142
|
+
ws.on("message", (raw) => {
|
|
143
|
+
try {
|
|
144
|
+
const data = JSON.parse(raw.toString());
|
|
145
|
+
if (data.type === "connected") {
|
|
146
|
+
this.emit("connected", data);
|
|
147
|
+
resolve();
|
|
148
|
+
} else if (data.type === "heartbeat") {
|
|
149
|
+
this.emit("heartbeat", data);
|
|
150
|
+
} else if (data.type === "error") {
|
|
151
|
+
this.emit("error", new Error(data.error));
|
|
152
|
+
}
|
|
153
|
+
this.emit(data.type, data);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
ws.on("close", () => {
|
|
158
|
+
if (!this.closed) {
|
|
159
|
+
this.scheduleReconnect();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
ws.on("error", (err) => {
|
|
163
|
+
if (this.reconnectAttempt === 0 && ws.readyState === WebSocket.CONNECTING) {
|
|
164
|
+
reject(err);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
scheduleReconnect() {
|
|
170
|
+
if (this.closed) return;
|
|
171
|
+
const delay = Math.min(
|
|
172
|
+
this.opts.reconnectBaseMs * Math.pow(2, this.reconnectAttempt),
|
|
173
|
+
this.opts.reconnectMaxMs
|
|
174
|
+
);
|
|
175
|
+
this.reconnectAttempt++;
|
|
176
|
+
this.reconnectTimer = setTimeout(() => {
|
|
177
|
+
if (!this.closed) {
|
|
178
|
+
this._connect().catch(() => {
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}, delay);
|
|
182
|
+
}
|
|
183
|
+
accept(negotiationId) {
|
|
184
|
+
this.send({ type: "accept_negotiation", negotiation_id: negotiationId });
|
|
185
|
+
}
|
|
186
|
+
decline(negotiationId, reason) {
|
|
187
|
+
this.send({ type: "decline_negotiation", negotiation_id: negotiationId, reason });
|
|
188
|
+
}
|
|
189
|
+
isConnected() {
|
|
190
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
191
|
+
}
|
|
192
|
+
send(data) {
|
|
193
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
194
|
+
this.ws.send(JSON.stringify(data));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
request(msg, responseType, timeoutMs = 3e4) {
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
if (!this.isConnected()) {
|
|
202
|
+
reject(new Error(`Lobby WS not connected (readyState: ${this.ws?.readyState ?? "null"}) \u2014 cannot send ${msg.type}`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const requestId = `req_${++requestCounter}_${Date.now()}`;
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
cleanup();
|
|
208
|
+
reject(new Error(`WS request timed out waiting for ${responseType} (sent ${msg.type}, waited ${timeoutMs}ms)`));
|
|
209
|
+
}, timeoutMs);
|
|
210
|
+
const handler = (data) => {
|
|
211
|
+
if (data.request_id === requestId) {
|
|
212
|
+
cleanup();
|
|
213
|
+
if (data.error) {
|
|
214
|
+
reject(new Error(data.error));
|
|
215
|
+
} else {
|
|
216
|
+
resolve(data);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const cleanup = () => {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
this.removeListener(responseType, handler);
|
|
223
|
+
};
|
|
224
|
+
this.on(responseType, handler);
|
|
225
|
+
const sent = this.send({ ...msg, request_id: requestId });
|
|
226
|
+
if (!sent) {
|
|
227
|
+
cleanup();
|
|
228
|
+
reject(new Error(`Failed to send ${msg.type} \u2014 WS connection dropped between check and send`));
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
close() {
|
|
233
|
+
this.closed = true;
|
|
234
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
235
|
+
if (this.ws) {
|
|
236
|
+
try {
|
|
237
|
+
this.ws.close();
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
this.ws = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/connector-session.ts
|
|
246
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
247
|
+
import WebSocket2 from "ws";
|
|
248
|
+
var NegotiationSession = class extends EventEmitter2 {
|
|
249
|
+
ws = null;
|
|
250
|
+
opts;
|
|
251
|
+
seenMessageIds = /* @__PURE__ */ new Set();
|
|
252
|
+
myAgentId;
|
|
253
|
+
constructor(opts) {
|
|
254
|
+
super();
|
|
255
|
+
this.opts = opts;
|
|
256
|
+
this.myAgentId = opts.agentId;
|
|
257
|
+
}
|
|
258
|
+
connect() {
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const url = `${this.opts.apiUrl}/v1/negotiations/${this.opts.negotiationId}/ws?token=${this.opts.token}`;
|
|
261
|
+
const ws = new WebSocket2(url);
|
|
262
|
+
this.ws = ws;
|
|
263
|
+
ws.on("message", (raw) => {
|
|
264
|
+
try {
|
|
265
|
+
const data = JSON.parse(raw.toString());
|
|
266
|
+
if (data.type === "connected") {
|
|
267
|
+
if (data.agent_id) this.myAgentId = data.agent_id;
|
|
268
|
+
this.emit("connected", data);
|
|
269
|
+
resolve();
|
|
270
|
+
} else if (data.type === "negotiation_message") {
|
|
271
|
+
const msg = data.message ?? {
|
|
272
|
+
id: data.id,
|
|
273
|
+
from_agent_id: data.from_agent_id,
|
|
274
|
+
type: data.message_type ?? data.type,
|
|
275
|
+
content: data.content,
|
|
276
|
+
terms: data.terms,
|
|
277
|
+
is_catchup: data.is_catchup,
|
|
278
|
+
created_at: data.created_at
|
|
279
|
+
};
|
|
280
|
+
const msgId = msg.id;
|
|
281
|
+
if (msgId && this.seenMessageIds.has(msgId)) return;
|
|
282
|
+
if (msgId) this.seenMessageIds.add(msgId);
|
|
283
|
+
if (msg.from_agent_id === this.myAgentId) return;
|
|
284
|
+
this.emit("incoming_message", msg);
|
|
285
|
+
} else if (data.type === "negotiation_started") {
|
|
286
|
+
this.emit("negotiation_started", data);
|
|
287
|
+
} else if (data.type === "negotiation_ended") {
|
|
288
|
+
this.emit("ended", data);
|
|
289
|
+
this.close();
|
|
290
|
+
} else if (data.type === "message_ack") {
|
|
291
|
+
if (data.message?.id) this.seenMessageIds.add(data.message.id);
|
|
292
|
+
this.emit("message_ack", data);
|
|
293
|
+
} else if (data.type === "agent_thinking") {
|
|
294
|
+
this.emit("agent_thinking", data);
|
|
295
|
+
} else if (data.type === "error") {
|
|
296
|
+
this.emit("error", new Error(data.error));
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
ws.on("error", (err) => {
|
|
302
|
+
if (ws.readyState === WebSocket2.CONNECTING) {
|
|
303
|
+
reject(err);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
ws.on("close", () => {
|
|
307
|
+
this.emit("disconnected");
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
send(msg) {
|
|
312
|
+
if (this.ws && this.ws.readyState === WebSocket2.OPEN) {
|
|
313
|
+
this.ws.send(JSON.stringify(msg));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
sendThinking() {
|
|
317
|
+
this.send({ type: "thinking" });
|
|
318
|
+
}
|
|
319
|
+
close() {
|
|
320
|
+
if (this.ws) {
|
|
321
|
+
try {
|
|
322
|
+
this.ws.close();
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
this.ws = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/service.ts
|
|
331
|
+
var lobbyClient = null;
|
|
332
|
+
function getLobbyClient() {
|
|
333
|
+
return lobbyClient;
|
|
334
|
+
}
|
|
335
|
+
var activeNegotiation = null;
|
|
336
|
+
var pluginRuntime = null;
|
|
337
|
+
function resolveAgentFromBindings(cfg, channel2) {
|
|
338
|
+
const bindings = cfg?.bindings;
|
|
339
|
+
if (!Array.isArray(bindings)) return null;
|
|
340
|
+
for (const b of bindings) {
|
|
341
|
+
if (b?.match?.channel === channel2 && b?.agentId) {
|
|
342
|
+
return b.agentId;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
var serviceStarted = false;
|
|
348
|
+
function setRuntime(rt) {
|
|
349
|
+
pluginRuntime = rt;
|
|
350
|
+
}
|
|
351
|
+
async function ensureServiceStarted(log) {
|
|
352
|
+
if (serviceStarted || lobbyClient) return;
|
|
353
|
+
if (!pluginRuntime) return;
|
|
354
|
+
try {
|
|
355
|
+
const cfg = await pluginRuntime.config.loadConfig();
|
|
356
|
+
const bouncerCfg = cfg?.channels?.bouncer;
|
|
357
|
+
if (!bouncerCfg?.role) return;
|
|
358
|
+
const config = {
|
|
359
|
+
role: bouncerCfg.role,
|
|
360
|
+
apiUrl: bouncerCfg.apiUrl,
|
|
361
|
+
apiKey: bouncerCfg.apiKey,
|
|
362
|
+
agentId: bouncerCfg.agentId,
|
|
363
|
+
autoAccept: bouncerCfg.autoAccept ?? true,
|
|
364
|
+
llmTimeoutMs: bouncerCfg.llmTimeoutMs ?? 25e3
|
|
365
|
+
};
|
|
366
|
+
serviceStarted = true;
|
|
367
|
+
log.info(`[bouncer] Self-starting service \u2014 role: ${config.role}, agent: ${config.agentId}`);
|
|
368
|
+
await startLobby(config, log);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
log.error(`[bouncer] Self-start failed: ${err.message}`);
|
|
371
|
+
serviceStarted = false;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function createBouncerService(config) {
|
|
375
|
+
return {
|
|
376
|
+
id: "bouncer-negotiation-service",
|
|
377
|
+
async start(ctx) {
|
|
378
|
+
const log = ctx.logger;
|
|
379
|
+
await startLobby(config, log);
|
|
380
|
+
},
|
|
381
|
+
async stop() {
|
|
382
|
+
if (lobbyClient) {
|
|
383
|
+
lobbyClient.close();
|
|
384
|
+
lobbyClient = null;
|
|
385
|
+
}
|
|
386
|
+
if (activeNegotiation) {
|
|
387
|
+
removeActiveSession(activeNegotiation.negotiationId);
|
|
388
|
+
activeNegotiation = null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
async function startLobby(config, log) {
|
|
394
|
+
const wsBase = config.apiUrl.replace(/^http/, "ws");
|
|
395
|
+
lobbyClient = new LobbyClient({
|
|
396
|
+
apiUrl: wsBase,
|
|
397
|
+
token: config.apiKey,
|
|
398
|
+
agentId: config.agentId,
|
|
399
|
+
reconnectBaseMs: 1e3,
|
|
400
|
+
reconnectMaxMs: 3e4
|
|
401
|
+
});
|
|
402
|
+
lobbyClient.on("connected", () => {
|
|
403
|
+
log.info(`[bouncer] Lobby connected (${config.role} mode).`);
|
|
404
|
+
});
|
|
405
|
+
if (config.role === "bouncer") {
|
|
406
|
+
lobbyClient.on("negotiation_requested", (data) => {
|
|
407
|
+
handleNegotiationRequested(data, config, wsBase, log);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
lobbyClient.on("heartbeat", () => {
|
|
411
|
+
log.debug("[bouncer] Lobby heartbeat");
|
|
412
|
+
});
|
|
413
|
+
try {
|
|
414
|
+
await lobbyClient.connect();
|
|
415
|
+
} catch (err) {
|
|
416
|
+
log.error(`[bouncer] Lobby connection failed: ${err.message}`);
|
|
417
|
+
throw err;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function handleNegotiationRequested(data, config, wsBase, log) {
|
|
421
|
+
const negId = data.negotiation_id;
|
|
422
|
+
if (activeNegotiation) {
|
|
423
|
+
log.info(`[bouncer] Declining ${negId} \u2014 already in negotiation ${activeNegotiation.negotiationId}`);
|
|
424
|
+
lobbyClient?.decline(negId, "busy \u2014 already in a negotiation");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!config.autoAccept) {
|
|
428
|
+
log.info(`[bouncer] Negotiation ${negId} requested but autoAccept is off. Declining.`);
|
|
429
|
+
lobbyClient?.decline(negId, "auto-accept disabled");
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
log.info(`[bouncer] Accepting negotiation ${negId}`);
|
|
433
|
+
lobbyClient?.accept(negId);
|
|
434
|
+
activeNegotiation = {
|
|
435
|
+
negotiationId: negId,
|
|
436
|
+
status: "pending",
|
|
437
|
+
otherAgentId: data.brand?.id,
|
|
438
|
+
offerContext: data.offer ? { product: data.offer.product, terms: data.offer.terms, opening_message: data.offer.opening_message } : void 0
|
|
439
|
+
};
|
|
440
|
+
await connectToNegotiationRoom(negId, config, wsBase, log);
|
|
441
|
+
}
|
|
442
|
+
async function connectToNegotiationRoom(negotiationId, config, wsBase, log) {
|
|
443
|
+
const session = new NegotiationSession({
|
|
444
|
+
apiUrl: wsBase,
|
|
445
|
+
token: config.apiKey,
|
|
446
|
+
negotiationId,
|
|
447
|
+
agentId: config.agentId
|
|
448
|
+
});
|
|
449
|
+
session.on("connected", () => {
|
|
450
|
+
log.info(`[bouncer] Connected to negotiation room ${negotiationId}`);
|
|
451
|
+
});
|
|
452
|
+
session.on("negotiation_started", (data) => {
|
|
453
|
+
log.info(`[bouncer] Negotiation ${negotiationId} started`);
|
|
454
|
+
if (activeNegotiation) {
|
|
455
|
+
activeNegotiation.status = "live";
|
|
456
|
+
if (data.queue_context) {
|
|
457
|
+
activeNegotiation.queueContext = data.queue_context;
|
|
458
|
+
}
|
|
459
|
+
if (data.relationship) {
|
|
460
|
+
activeNegotiation.relationship = data.relationship;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
let dispatching = false;
|
|
465
|
+
session.on("incoming_message", (msg) => {
|
|
466
|
+
if (dispatching) {
|
|
467
|
+
log.debug(`[bouncer] Skipping message while dispatch in progress`);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
log.info(`[bouncer] Message from ${msg.from_agent_id}: ${(msg.content ?? "").slice(0, 80)}...`);
|
|
471
|
+
dispatching = true;
|
|
472
|
+
dispatchToAgent(negotiationId, msg, config, session, log).finally(() => {
|
|
473
|
+
dispatching = false;
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
session.on("agent_thinking", () => {
|
|
477
|
+
log.debug(`[bouncer] Other agent thinking in ${negotiationId}`);
|
|
478
|
+
});
|
|
479
|
+
session.on("ended", (data) => {
|
|
480
|
+
log.info(`[bouncer] Negotiation ${negotiationId} ended: ${data?.status ?? "unknown"}`);
|
|
481
|
+
removeActiveSession(negotiationId);
|
|
482
|
+
activeNegotiation = null;
|
|
483
|
+
});
|
|
484
|
+
session.on("disconnected", () => {
|
|
485
|
+
log.warn(`[bouncer] Disconnected from negotiation ${negotiationId}`);
|
|
486
|
+
});
|
|
487
|
+
setActiveSession(negotiationId, session);
|
|
488
|
+
try {
|
|
489
|
+
await session.connect();
|
|
490
|
+
} catch (err) {
|
|
491
|
+
log.error(`[bouncer] Failed to connect to negotiation ${negotiationId}: ${err.message}`);
|
|
492
|
+
removeActiveSession(negotiationId);
|
|
493
|
+
activeNegotiation = null;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function dispatchToAgent(negotiationId, msg, config, session, log) {
|
|
497
|
+
if (!pluginRuntime) {
|
|
498
|
+
log.error("[bouncer] Runtime not set \u2014 cannot dispatch inbound message to agent");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const rt = pluginRuntime;
|
|
502
|
+
try {
|
|
503
|
+
const currentCfg = await rt.config.loadConfig();
|
|
504
|
+
const resolvedAgent = resolveAgentFromBindings(currentCfg, "bouncer");
|
|
505
|
+
if (resolvedAgent) {
|
|
506
|
+
log.info(`[bouncer] Routing negotiation ${negotiationId} to agent: ${resolvedAgent}`);
|
|
507
|
+
}
|
|
508
|
+
const msgCtx = rt.channel.reply.finalizeInboundContext({
|
|
509
|
+
Body: msg.content ?? "",
|
|
510
|
+
RawBody: msg.content ?? "",
|
|
511
|
+
CommandBody: msg.content ?? "",
|
|
512
|
+
From: `bouncer:${msg.from_agent_id}`,
|
|
513
|
+
To: `bouncer:${config.agentId}`,
|
|
514
|
+
SessionKey: `bouncer:${negotiationId}`,
|
|
515
|
+
AccountId: config.agentId,
|
|
516
|
+
OriginatingChannel: "bouncer",
|
|
517
|
+
OriginatingTo: `bouncer:${msg.from_agent_id}`,
|
|
518
|
+
ChatType: "direct",
|
|
519
|
+
SenderName: msg.from_agent_id,
|
|
520
|
+
SenderId: msg.from_agent_id,
|
|
521
|
+
Provider: "bouncer",
|
|
522
|
+
Surface: "bouncer",
|
|
523
|
+
ConversationLabel: `Negotiation ${negotiationId}`,
|
|
524
|
+
Timestamp: Date.now(),
|
|
525
|
+
CommandAuthorized: true,
|
|
526
|
+
...resolvedAgent ? { AgentId: resolvedAgent, TargetAgentId: resolvedAgent, ResolvedAgentId: resolvedAgent } : {}
|
|
527
|
+
});
|
|
528
|
+
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
529
|
+
ctx: msgCtx,
|
|
530
|
+
cfg: currentCfg,
|
|
531
|
+
dispatcherOptions: {
|
|
532
|
+
deliver: async (payload) => {
|
|
533
|
+
const text = payload?.text ?? payload?.body;
|
|
534
|
+
if (!text) return;
|
|
535
|
+
const parsed = parseResponse(text);
|
|
536
|
+
session.sendThinking();
|
|
537
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
538
|
+
session.send(parsed);
|
|
539
|
+
log.info(`[bouncer] Agent replied (${parsed.type}): ${parsed.content.slice(0, 80)}...`);
|
|
540
|
+
},
|
|
541
|
+
onReplyStart: () => {
|
|
542
|
+
session.sendThinking();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
} catch (err) {
|
|
547
|
+
log.error(`[bouncer] Dispatch failed for negotiation ${negotiationId}: ${err.message}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/types.ts
|
|
552
|
+
function resolveConfig(pluginConfig) {
|
|
553
|
+
if (!pluginConfig) throw new Error("Bouncer plugin config is required");
|
|
554
|
+
const role = pluginConfig.role;
|
|
555
|
+
if (role !== "bouncer" && role !== "brand") {
|
|
556
|
+
throw new Error(`Invalid role "${role}" \u2014 must be "bouncer" or "brand"`);
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
role,
|
|
560
|
+
apiUrl: pluginConfig.apiUrl,
|
|
561
|
+
apiKey: pluginConfig.apiKey,
|
|
562
|
+
agentId: pluginConfig.agentId,
|
|
563
|
+
autoAccept: pluginConfig.autoAccept ?? true,
|
|
564
|
+
llmTimeoutMs: pluginConfig.llmTimeoutMs ?? 25e3
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function wsUrl(apiUrl) {
|
|
568
|
+
return apiUrl.replace(/^http/, "ws");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/tools.ts
|
|
572
|
+
function createBrandTools(config, logger) {
|
|
573
|
+
if (config.role !== "brand") return [];
|
|
574
|
+
const bouncerBrowse = {
|
|
575
|
+
name: "bouncer_browse",
|
|
576
|
+
description: "Browse available Bouncer agents in the directory. Returns a list of bouncers with their name, listing, tags, and reputation score. Use this to find a bouncer to target with an offer.",
|
|
577
|
+
parameters: {
|
|
578
|
+
type: "object",
|
|
579
|
+
properties: {
|
|
580
|
+
limit: {
|
|
581
|
+
type: "number",
|
|
582
|
+
description: "Max results to return (default: 20)"
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
async execute(params) {
|
|
587
|
+
const lobby = getLobbyClient();
|
|
588
|
+
if (!lobby) return { error: "Not connected to lobby" };
|
|
589
|
+
try {
|
|
590
|
+
const result = await lobby.request(
|
|
591
|
+
{ type: "browse_bouncers", limit: params.limit ?? 20 },
|
|
592
|
+
"browse_bouncers_result"
|
|
593
|
+
);
|
|
594
|
+
logger.info(`[bouncer] Browse returned ${result.bouncers?.length ?? 0} bouncers`);
|
|
595
|
+
return result.bouncers;
|
|
596
|
+
} catch (err) {
|
|
597
|
+
return { error: `Browse failed: ${err.message}` };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const bouncerProfile = {
|
|
602
|
+
name: "bouncer_profile",
|
|
603
|
+
description: "Get the full profile of a specific Bouncer agent, including their listing, enrichment data, engagement rules, and verified credentials. Use this before crafting a personalized offer.",
|
|
604
|
+
parameters: {
|
|
605
|
+
type: "object",
|
|
606
|
+
properties: {
|
|
607
|
+
bouncer_id: {
|
|
608
|
+
type: "string",
|
|
609
|
+
description: "The Bouncer agent's UUID"
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
required: ["bouncer_id"]
|
|
613
|
+
},
|
|
614
|
+
async execute(params) {
|
|
615
|
+
const lobby = getLobbyClient();
|
|
616
|
+
if (!lobby) return { error: "Not connected to lobby" };
|
|
617
|
+
try {
|
|
618
|
+
const result = await lobby.request(
|
|
619
|
+
{ type: "get_profile", bouncer_id: params.bouncer_id },
|
|
620
|
+
"get_profile_result"
|
|
621
|
+
);
|
|
622
|
+
if (result.error) return { error: result.error };
|
|
623
|
+
return result.profile;
|
|
624
|
+
} catch (err) {
|
|
625
|
+
return { error: `Profile fetch failed: ${err.message}` };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
const bouncerOffer = {
|
|
630
|
+
name: "bouncer_offer",
|
|
631
|
+
description: "Submit an offer to a Bouncer agent and create a negotiation. This sends your offer and automatically connects you to the negotiation room when the bouncer accepts. Returns the offer_id and negotiation_id.",
|
|
632
|
+
parameters: {
|
|
633
|
+
type: "object",
|
|
634
|
+
properties: {
|
|
635
|
+
bouncer_id: {
|
|
636
|
+
type: "string",
|
|
637
|
+
description: "The target Bouncer's UUID"
|
|
638
|
+
},
|
|
639
|
+
product: {
|
|
640
|
+
type: "string",
|
|
641
|
+
description: "Product or service being offered"
|
|
642
|
+
},
|
|
643
|
+
terms: {
|
|
644
|
+
type: "string",
|
|
645
|
+
description: "Deal terms (e.g. '$20 credit + free delivery')"
|
|
646
|
+
},
|
|
647
|
+
opening_message: {
|
|
648
|
+
type: "string",
|
|
649
|
+
description: "Personalized opening message to the Bouncer"
|
|
650
|
+
},
|
|
651
|
+
priority_bid: {
|
|
652
|
+
type: "number",
|
|
653
|
+
description: "Priority bid in USDC (e.g. 2.0)"
|
|
654
|
+
},
|
|
655
|
+
arena: {
|
|
656
|
+
type: "boolean",
|
|
657
|
+
description: "Practice mode \u2014 no real USDC (default: false)"
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
required: ["bouncer_id", "product", "terms", "opening_message", "priority_bid"]
|
|
661
|
+
},
|
|
662
|
+
async execute(params) {
|
|
663
|
+
const lobby = getLobbyClient();
|
|
664
|
+
if (!lobby) return { error: "Not connected to lobby" };
|
|
665
|
+
const bouncerId = params.bouncer_id;
|
|
666
|
+
try {
|
|
667
|
+
logger.info(`[bouncer] Submitting offer to ${bouncerId}`);
|
|
668
|
+
const offerResult = await lobby.request(
|
|
669
|
+
{
|
|
670
|
+
type: "submit_offer",
|
|
671
|
+
bouncer_id: bouncerId,
|
|
672
|
+
product: params.product,
|
|
673
|
+
terms: params.terms,
|
|
674
|
+
opening_message: params.opening_message,
|
|
675
|
+
priority_bid: params.priority_bid,
|
|
676
|
+
arena: params.arena ?? false
|
|
677
|
+
},
|
|
678
|
+
"submit_offer_result"
|
|
679
|
+
);
|
|
680
|
+
if (offerResult.error) {
|
|
681
|
+
return { error: `Offer submission failed: ${offerResult.error}` };
|
|
682
|
+
}
|
|
683
|
+
const offerId = offerResult.offer_id;
|
|
684
|
+
logger.info(`[bouncer] Offer submitted: ${offerId}`);
|
|
685
|
+
logger.info(`[bouncer] Creating negotiation for offer ${offerId}`);
|
|
686
|
+
const negResult = await lobby.request(
|
|
687
|
+
{ type: "create_negotiation", offer_id: offerId, bouncer_id: bouncerId },
|
|
688
|
+
"create_negotiation_result"
|
|
689
|
+
);
|
|
690
|
+
if (negResult.error) {
|
|
691
|
+
return { error: `Negotiation creation failed: ${negResult.error}`, offer_id: offerId };
|
|
692
|
+
}
|
|
693
|
+
const negotiationId = negResult.negotiation_id;
|
|
694
|
+
logger.info(`[bouncer] Negotiation created: ${negotiationId}`);
|
|
695
|
+
logger.info(`[bouncer] Waiting for bouncer to accept (via WS)...`);
|
|
696
|
+
const accepted = await waitForAcceptanceWS(negotiationId, 12e4);
|
|
697
|
+
if (!accepted) {
|
|
698
|
+
return { status: "declined_or_timeout", offer_id: offerId, negotiation_id: negotiationId };
|
|
699
|
+
}
|
|
700
|
+
logger.info(`[bouncer] Accepted! Connecting to negotiation room...`);
|
|
701
|
+
await connectToNegotiationRoom(negotiationId, config, wsUrl(config.apiUrl), logger);
|
|
702
|
+
return {
|
|
703
|
+
status: "connected",
|
|
704
|
+
offer_id: offerId,
|
|
705
|
+
negotiation_id: negotiationId,
|
|
706
|
+
message: "Connected to negotiation room. Incoming messages will appear as chat messages."
|
|
707
|
+
};
|
|
708
|
+
} catch (err) {
|
|
709
|
+
return { error: `Offer flow failed: ${err.message}` };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
return [bouncerBrowse, bouncerProfile, bouncerOffer];
|
|
714
|
+
}
|
|
715
|
+
function waitForAcceptanceWS(negotiationId, timeoutMs) {
|
|
716
|
+
return new Promise((resolve) => {
|
|
717
|
+
const lobby = getLobbyClient();
|
|
718
|
+
if (!lobby) {
|
|
719
|
+
resolve(false);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const timer = setTimeout(() => {
|
|
723
|
+
cleanup();
|
|
724
|
+
resolve(false);
|
|
725
|
+
}, timeoutMs);
|
|
726
|
+
const onAccepted = (data) => {
|
|
727
|
+
if (data.negotiation_id === negotiationId) {
|
|
728
|
+
cleanup();
|
|
729
|
+
resolve(true);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const onDeclined = (data) => {
|
|
733
|
+
if (data.negotiation_id === negotiationId) {
|
|
734
|
+
cleanup();
|
|
735
|
+
resolve(false);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
const cleanup = () => {
|
|
739
|
+
clearTimeout(timer);
|
|
740
|
+
lobby.removeListener("negotiation_accepted", onAccepted);
|
|
741
|
+
lobby.removeListener("negotiation_declined", onDeclined);
|
|
742
|
+
};
|
|
743
|
+
lobby.on("negotiation_accepted", onAccepted);
|
|
744
|
+
lobby.on("negotiation_declined", onDeclined);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// index.ts
|
|
749
|
+
var pluginLogger = {
|
|
750
|
+
info: console.log,
|
|
751
|
+
warn: console.warn,
|
|
752
|
+
error: console.error,
|
|
753
|
+
debug: () => {
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
var channel = createBouncerChannel(pluginLogger);
|
|
757
|
+
var index_default = definePluginEntry({
|
|
758
|
+
id: "bouncer",
|
|
759
|
+
name: "Bouncer Protocol",
|
|
760
|
+
description: "Agent-to-agent negotiation channel via the Bouncer Protocol. Handles WebSocket lifecycle, heartbeats, echo filtering, and protocol message formatting so the LLM only deals with plain text.",
|
|
761
|
+
register(api) {
|
|
762
|
+
const logger = api.logger ?? pluginLogger;
|
|
763
|
+
pluginLogger = logger;
|
|
764
|
+
api.registerChannel({ plugin: channel });
|
|
765
|
+
setRuntime(api.runtime);
|
|
766
|
+
if (api.runtime.logging) {
|
|
767
|
+
const child = api.runtime.logging.getChildLogger("bouncer");
|
|
768
|
+
if (child) pluginLogger = child;
|
|
769
|
+
}
|
|
770
|
+
const bouncerConfig = resolveChannelConfig(api);
|
|
771
|
+
if (!bouncerConfig) {
|
|
772
|
+
logger.warn("[bouncer] No bouncer channel config found \u2014 will self-start from runtime config.");
|
|
773
|
+
setTimeout(() => {
|
|
774
|
+
ensureServiceStartedWithTools(api, pluginLogger).catch((err) => {
|
|
775
|
+
pluginLogger.error(`[bouncer] Deferred start failed: ${err.message}`);
|
|
776
|
+
});
|
|
777
|
+
}, 2e3);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const config = resolveConfig(bouncerConfig);
|
|
781
|
+
logger.info(`[bouncer] Registering plugin \u2014 role: ${config.role}, agent: ${config.agentId}`);
|
|
782
|
+
api.registerService(createBouncerService(config));
|
|
783
|
+
const tools = createBrandTools(config, logger);
|
|
784
|
+
for (const tool of tools) {
|
|
785
|
+
api.registerTool(tool, { name: tool.name });
|
|
786
|
+
}
|
|
787
|
+
logger.info(`[bouncer] Plugin registered. Role: ${config.role}`);
|
|
788
|
+
if (config.role === "bouncer") {
|
|
789
|
+
logger.info("[bouncer] Lobby service will start on gateway boot.");
|
|
790
|
+
} else {
|
|
791
|
+
logger.info("[bouncer] Brand tools registered: bouncer_browse, bouncer_profile, bouncer_offer");
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
async function ensureServiceStartedWithTools(api, log) {
|
|
796
|
+
try {
|
|
797
|
+
const cfg = await api.runtime.config.loadConfig();
|
|
798
|
+
const bouncerCfg = cfg?.channels?.bouncer;
|
|
799
|
+
if (!bouncerCfg?.role) {
|
|
800
|
+
log.warn("[bouncer] Deferred start: no channels.bouncer.role found in runtime config.");
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const config = resolveConfig(bouncerCfg);
|
|
804
|
+
log.info(`[bouncer] Deferred registration \u2014 role: ${config.role}, agent: ${config.agentId}`);
|
|
805
|
+
api.registerService(createBouncerService(config));
|
|
806
|
+
const tools = createBrandTools(config, log);
|
|
807
|
+
for (const tool of tools) {
|
|
808
|
+
api.registerTool(tool, { name: tool.name });
|
|
809
|
+
}
|
|
810
|
+
if (config.role === "bouncer") {
|
|
811
|
+
log.info("[bouncer] Lobby service registered via deferred start.");
|
|
812
|
+
} else {
|
|
813
|
+
log.info("[bouncer] Brand tools registered via deferred start: bouncer_browse, bouncer_profile, bouncer_offer");
|
|
814
|
+
}
|
|
815
|
+
await ensureServiceStarted(log);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
log.error(`[bouncer] Deferred start failed: ${err.message}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function resolveChannelConfig(api) {
|
|
821
|
+
const fromChannels = api.config?.channels?.bouncer;
|
|
822
|
+
if (fromChannels && fromChannels.role) return fromChannels;
|
|
823
|
+
if (api.pluginConfig && Object.keys(api.pluginConfig).length > 0) {
|
|
824
|
+
return api.pluginConfig;
|
|
825
|
+
}
|
|
826
|
+
if (fromChannels && !fromChannels.role) {
|
|
827
|
+
pluginLogger.warn(
|
|
828
|
+
"[bouncer] channels.bouncer exists but has no 'role' \u2014 check config. Required: role, apiUrl, apiKey, agentId"
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
export {
|
|
834
|
+
index_default as default
|
|
835
|
+
};
|
|
836
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sdk-types.ts","../src/connector-parser.ts","../src/bouncer-channel.ts","../src/connector-lobby.ts","../src/connector-session.ts","../src/service.ts","../src/types.ts","../src/tools.ts","../index.ts"],"sourcesContent":["/**\n * Minimal type stubs for the OpenClaw Plugin SDK.\n *\n * When installed into an OpenClaw environment, replace these with:\n * import { definePluginEntry } from \"openclaw/plugin-sdk/core\";\n *\n * These stubs match the contract from openclaw/openclaw src/plugin-sdk/core.ts\n * and src/plugins/types.ts as of 2026-03.\n */\n\nexport interface PluginLogger {\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n debug(msg: string, ...args: unknown[]): void;\n}\n\nexport interface ReplyPayload {\n text?: string;\n body?: string;\n mediaUrl?: string;\n mediaUrls?: string[];\n [key: string]: unknown;\n}\n\nexport interface MsgContext {\n Body?: string;\n BodyForAgent?: string;\n RawBody?: string;\n CommandBody?: string;\n From?: string;\n To?: string;\n SessionKey?: string;\n AccountId?: string;\n OriginatingChannel?: string;\n OriginatingTo?: string;\n ChatType?: string;\n SenderName?: string;\n SenderId?: string;\n Provider?: string;\n Surface?: string;\n ConversationLabel?: string;\n Timestamp?: number;\n CommandAuthorized?: boolean;\n [key: string]: unknown;\n}\n\nexport type FinalizedMsgContext = Omit<MsgContext, \"CommandAuthorized\"> & {\n CommandAuthorized: boolean;\n};\n\nexport interface ReplyDispatcherWithTypingOptions {\n deliver: (payload: ReplyPayload, info: { kind: string }) => Promise<void>;\n onReplyStart?: () => Promise<void> | void;\n onIdle?: () => void;\n onCleanup?: () => void;\n [key: string]: unknown;\n}\n\nexport interface DispatchInboundResult {\n [key: string]: unknown;\n}\n\nexport interface PluginRuntime {\n channel: {\n reply: {\n finalizeInboundContext: <T extends Record<string, unknown>>(ctx: T) => T & FinalizedMsgContext;\n dispatchReplyWithBufferedBlockDispatcher: (params: {\n ctx: MsgContext | FinalizedMsgContext;\n cfg: OpenClawConfig;\n dispatcherOptions: ReplyDispatcherWithTypingOptions;\n replyOptions?: Record<string, unknown>;\n }) => Promise<DispatchInboundResult>;\n dispatchReplyFromConfig?: (...args: unknown[]) => Promise<unknown>;\n };\n debounce: {\n createInboundDebouncer: (...args: unknown[]) => unknown;\n resolveInboundDebounceMs: (...args: unknown[]) => number;\n };\n session: {\n recordInboundSession: (...args: unknown[]) => unknown;\n [key: string]: unknown;\n };\n [key: string]: unknown;\n };\n config: {\n loadConfig: () => Promise<OpenClawConfig>;\n writeConfigFile?: (...args: unknown[]) => Promise<void>;\n };\n logging: { shouldLogVerbose(): boolean; getChildLogger(name: string): PluginLogger };\n state: { resolveStateDir(): string };\n}\n\nexport interface OpenClawConfig {\n channels?: Record<string, Record<string, unknown>>;\n plugins?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\nexport interface OpenClawPluginServiceContext {\n config: OpenClawConfig;\n workspaceDir?: string;\n stateDir: string;\n logger: PluginLogger;\n}\n\nexport interface OpenClawPluginService {\n id: string;\n start: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;\n stop?: (ctx: OpenClawPluginServiceContext) => void | Promise<void>;\n}\n\nexport interface OpenClawPluginToolContext {\n config?: OpenClawConfig;\n agentId?: string;\n sessionKey?: string;\n messageChannel?: string;\n [key: string]: unknown;\n}\n\nexport interface AgentTool {\n name: string;\n description: string;\n parameters?: Record<string, unknown>;\n execute: (params: Record<string, unknown>, ctx?: unknown) => Promise<unknown>;\n}\n\nexport type OpenClawPluginToolFactory = (ctx: OpenClawPluginToolContext) => AgentTool | AgentTool[] | null | undefined;\n\nexport interface OpenClawPluginApi {\n id: string;\n name: string;\n version?: string;\n description?: string;\n source: string;\n rootDir?: string;\n registrationMode: \"full\" | \"setup-only\" | \"setup-runtime\";\n config: OpenClawConfig;\n pluginConfig?: Record<string, unknown>;\n runtime: PluginRuntime;\n logger: PluginLogger;\n\n registerTool: (tool: AgentTool | OpenClawPluginToolFactory, opts?: { name?: string; names?: string[]; optional?: boolean }) => void;\n registerHook: (events: string | string[], handler: (...args: unknown[]) => void | Promise<void>, opts?: Record<string, unknown>) => void;\n registerHttpRoute: (params: Record<string, unknown>) => void;\n registerChannel: (registration: { plugin: ChannelPlugin } | ChannelPlugin) => void;\n registerGatewayMethod: (method: string, handler: (...args: unknown[]) => unknown) => void;\n registerService: (service: OpenClawPluginService) => void;\n registerCommand: (command: { name: string; description: string; handler: (...args: unknown[]) => void | Promise<void> }) => void;\n resolvePath: (input: string) => string;\n on: (hookName: string, handler: (...args: unknown[]) => void | Promise<void>, opts?: { priority?: number }) => void;\n}\n\nexport interface ChannelMeta {\n id: string;\n label: string;\n selectionLabel: string;\n docsPath?: string;\n blurb?: string;\n aliases?: string[];\n order?: number;\n}\n\nexport interface ChannelCapabilities {\n chatTypes: Array<\"direct\" | \"group\">;\n}\n\nexport interface ChannelConfigAdapter {\n listAccountIds: (cfg: OpenClawConfig) => string[];\n resolveAccount: (cfg: OpenClawConfig, accountId?: string) => Record<string, unknown>;\n}\n\nexport interface ChannelOutboundAdapter {\n deliveryMode: \"direct\" | \"queued\";\n sendText: (ctx: SendTextContext) => Promise<{ ok: boolean; error?: string }>;\n}\n\nexport interface SendTextContext {\n text: string;\n conversationId?: string;\n accountId?: string;\n replyTo?: string;\n [key: string]: unknown;\n}\n\nexport interface ChannelLifecycleAdapter {\n startAccount?: (accountId: string, config: Record<string, unknown>, logger: PluginLogger) => Promise<void>;\n stopAccount?: (accountId: string) => Promise<void>;\n}\n\nexport interface ChannelPlugin {\n id: string;\n meta: ChannelMeta;\n capabilities: ChannelCapabilities;\n config: ChannelConfigAdapter;\n outbound?: ChannelOutboundAdapter;\n lifecycle?: ChannelLifecycleAdapter;\n [key: string]: unknown;\n}\n\nexport interface DefinedPluginEntry {\n id: string;\n name: string;\n description: string;\n register: (api: OpenClawPluginApi) => void;\n}\n\nexport function definePluginEntry(opts: {\n id: string;\n name: string;\n description: string;\n register: (api: OpenClawPluginApi) => void;\n}): DefinedPluginEntry {\n return {\n id: opts.id,\n name: opts.name,\n description: opts.description,\n register: opts.register,\n };\n}\n\nexport function defineChannelPluginEntry<TPlugin extends ChannelPlugin>(opts: {\n id: string;\n name: string;\n description: string;\n plugin: TPlugin;\n setRuntime?: (runtime: PluginRuntime) => void;\n registerFull?: (api: OpenClawPluginApi) => void;\n}): DefinedPluginEntry {\n return definePluginEntry({\n id: opts.id,\n name: opts.name,\n description: opts.description,\n register(api) {\n api.registerChannel({ plugin: opts.plugin });\n if (opts.setRuntime) opts.setRuntime(api.runtime);\n if (api.registrationMode === \"full\" && opts.registerFull) {\n opts.registerFull(api);\n }\n },\n });\n}\n","export interface BounceMessage {\n type: \"message\" | \"accept\" | \"walk\" | \"propose_terms\" | \"counter_terms\" | \"research\";\n content: string;\n terms?: Record<string, any>;\n}\n\nconst SIMPLE_PREFIXES: Record<string, BounceMessage[\"type\"]> = {\n \"[ACCEPT]\": \"accept\",\n \"[WALK]\": \"walk\",\n \"[RESEARCH]\": \"research\",\n};\n\nconst TERMS_PREFIXES: Record<string, BounceMessage[\"type\"]> = {\n \"[TERMS]\": \"propose_terms\",\n \"[COUNTER]\": \"counter_terms\",\n};\n\nexport function parseResponse(text: string): BounceMessage {\n const trimmed = text.trim();\n\n for (const [prefix, type] of Object.entries(SIMPLE_PREFIXES)) {\n if (trimmed.startsWith(prefix)) {\n return { type, content: trimmed.slice(prefix.length).trim() };\n }\n }\n\n for (const [prefix, type] of Object.entries(TERMS_PREFIXES)) {\n if (trimmed.startsWith(prefix)) {\n const rest = trimmed.slice(prefix.length);\n const jsonEnd = rest.indexOf(\"}\");\n if (jsonEnd !== -1) {\n const jsonStr = rest.slice(0, jsonEnd + 1);\n try {\n const terms = JSON.parse(jsonStr);\n const content = rest.slice(jsonEnd + 1).trim();\n return { type, content, terms };\n } catch {\n return { type: \"message\", content: trimmed };\n }\n }\n return { type: \"message\", content: trimmed };\n }\n }\n\n return { type: \"message\", content: trimmed };\n}\n","import { parseResponse } from \"./connector-parser.js\";\nimport { NegotiationSession } from \"./connector-session.js\";\nimport type {\n ChannelPlugin,\n OpenClawConfig,\n PluginLogger,\n SendTextContext,\n} from \"./sdk-types.js\";\n\nconst activeSessions = new Map<string, NegotiationSession>();\n\nexport function getActiveSession(negotiationId: string): NegotiationSession | undefined {\n return activeSessions.get(negotiationId);\n}\n\nexport function setActiveSession(negotiationId: string, session: NegotiationSession): void {\n activeSessions.set(negotiationId, session);\n}\n\nexport function removeActiveSession(negotiationId: string): void {\n const session = activeSessions.get(negotiationId);\n if (session) {\n session.close();\n activeSessions.delete(negotiationId);\n }\n}\n\nexport function getActiveNegotiationId(): string | undefined {\n const entries = [...activeSessions.entries()];\n return entries.length > 0 ? entries[0]![0] : undefined;\n}\n\nexport function createBouncerChannel(logger: PluginLogger): ChannelPlugin {\n return {\n id: \"bouncer\",\n\n meta: {\n id: \"bouncer\",\n label: \"Bouncer Protocol\",\n selectionLabel: \"Bouncer Protocol (Agent Negotiation)\",\n docsPath: \"/channels/bouncer\",\n blurb: \"Real-time agent-to-agent negotiation via the Bouncer Protocol\",\n aliases: [\"bouncer-protocol\"],\n order: 90,\n },\n\n capabilities: {\n chatTypes: [\"direct\"],\n },\n\n config: {\n listAccountIds(cfg: OpenClawConfig): string[] {\n const bouncer = cfg.channels?.bouncer as Record<string, unknown> | undefined;\n if (!bouncer?.agentId) return [];\n return [bouncer.agentId as string];\n },\n\n resolveAccount(cfg: OpenClawConfig, accountId?: string): Record<string, unknown> {\n const bouncer = cfg.channels?.bouncer as Record<string, unknown> | undefined;\n return bouncer ?? { accountId };\n },\n },\n\n outbound: {\n deliveryMode: \"direct\",\n\n async sendText(ctx: SendTextContext): Promise<{ ok: boolean; error?: string }> {\n const negId = ctx.conversationId ?? getActiveNegotiationId();\n if (!negId) {\n return { ok: false, error: \"No active negotiation session\" };\n }\n\n const session = activeSessions.get(negId);\n if (!session) {\n return { ok: false, error: `No session for negotiation ${negId}` };\n }\n\n const parsed = parseResponse(ctx.text);\n\n session.sendThinking();\n\n await new Promise((r) => setTimeout(r, 200));\n\n session.send(parsed);\n logger.info(`[bouncer] Sent ${parsed.type}: ${parsed.content.slice(0, 80)}...`);\n\n return { ok: true };\n },\n },\n };\n}\n","import { EventEmitter } from \"events\";\nimport WebSocket from \"ws\";\n\nlet requestCounter = 0;\n\nexport interface LobbyClientOpts {\n apiUrl: string;\n token: string;\n agentId: string;\n reconnectBaseMs?: number;\n reconnectMaxMs?: number;\n}\n\nexport class LobbyClient extends EventEmitter {\n private ws: WebSocket | null = null;\n private opts: Required<LobbyClientOpts>;\n private reconnectAttempt = 0;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private closed = false;\n\n constructor(opts: LobbyClientOpts) {\n super();\n this.opts = {\n reconnectBaseMs: 1000,\n reconnectMaxMs: 30_000,\n ...opts,\n };\n }\n\n connect(): Promise<void> {\n this.closed = false;\n return this._connect();\n }\n\n private _connect(): Promise<void> {\n return new Promise((resolve, reject) => {\n const url = `${this.opts.apiUrl}/v1/agents/${this.opts.agentId}/ws?token=${this.opts.token}`;\n const ws = new WebSocket(url);\n this.ws = ws;\n\n ws.on(\"open\", () => {\n this.reconnectAttempt = 0;\n });\n\n ws.on(\"message\", (raw) => {\n try {\n const data = JSON.parse(raw.toString());\n if (data.type === \"connected\") {\n this.emit(\"connected\", data);\n resolve();\n } else if (data.type === \"heartbeat\") {\n this.emit(\"heartbeat\", data);\n } else if (data.type === \"error\") {\n this.emit(\"error\", new Error(data.error));\n }\n this.emit(data.type, data);\n } catch {}\n });\n\n ws.on(\"close\", () => {\n if (!this.closed) {\n this.scheduleReconnect();\n }\n });\n\n ws.on(\"error\", (err) => {\n if (this.reconnectAttempt === 0 && ws.readyState === WebSocket.CONNECTING) {\n reject(err);\n }\n });\n });\n }\n\n private scheduleReconnect() {\n if (this.closed) return;\n const delay = Math.min(\n this.opts.reconnectBaseMs * Math.pow(2, this.reconnectAttempt),\n this.opts.reconnectMaxMs\n );\n this.reconnectAttempt++;\n this.reconnectTimer = setTimeout(() => {\n if (!this.closed) {\n this._connect().catch(() => {});\n }\n }, delay);\n }\n\n accept(negotiationId: string): void {\n this.send({ type: \"accept_negotiation\", negotiation_id: negotiationId });\n }\n\n decline(negotiationId: string, reason?: string): void {\n this.send({ type: \"decline_negotiation\", negotiation_id: negotiationId, reason });\n }\n\n isConnected(): boolean {\n return this.ws !== null && this.ws.readyState === WebSocket.OPEN;\n }\n\n send(data: any): boolean {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(data));\n return true;\n }\n return false;\n }\n\n request(msg: Record<string, unknown>, responseType: string, timeoutMs = 30_000): Promise<any> {\n return new Promise((resolve, reject) => {\n if (!this.isConnected()) {\n reject(new Error(`Lobby WS not connected (readyState: ${this.ws?.readyState ?? \"null\"}) — cannot send ${msg.type}`));\n return;\n }\n\n const requestId = `req_${++requestCounter}_${Date.now()}`;\n const timer = setTimeout(() => {\n cleanup();\n reject(new Error(`WS request timed out waiting for ${responseType} (sent ${msg.type}, waited ${timeoutMs}ms)`));\n }, timeoutMs);\n\n const handler = (data: any) => {\n if (data.request_id === requestId) {\n cleanup();\n if (data.error) {\n reject(new Error(data.error));\n } else {\n resolve(data);\n }\n }\n };\n\n const cleanup = () => {\n clearTimeout(timer);\n this.removeListener(responseType, handler);\n };\n\n this.on(responseType, handler);\n const sent = this.send({ ...msg, request_id: requestId });\n if (!sent) {\n cleanup();\n reject(new Error(`Failed to send ${msg.type} — WS connection dropped between check and send`));\n }\n });\n }\n\n close(): void {\n this.closed = true;\n if (this.reconnectTimer) clearTimeout(this.reconnectTimer);\n if (this.ws) {\n try { this.ws.close(); } catch {}\n this.ws = null;\n }\n }\n}\n","import { EventEmitter } from \"events\";\nimport WebSocket from \"ws\";\n\nexport interface NegotiationSessionOpts {\n apiUrl: string;\n token: string;\n negotiationId: string;\n agentId: string;\n}\n\nexport class NegotiationSession extends EventEmitter {\n private ws: WebSocket | null = null;\n private opts: NegotiationSessionOpts;\n private seenMessageIds = new Set<string>();\n private myAgentId: string;\n\n constructor(opts: NegotiationSessionOpts) {\n super();\n this.opts = opts;\n this.myAgentId = opts.agentId;\n }\n\n connect(): Promise<void> {\n return new Promise((resolve, reject) => {\n const url = `${this.opts.apiUrl}/v1/negotiations/${this.opts.negotiationId}/ws?token=${this.opts.token}`;\n const ws = new WebSocket(url);\n this.ws = ws;\n\n ws.on(\"message\", (raw) => {\n try {\n const data = JSON.parse(raw.toString());\n\n if (data.type === \"connected\") {\n if (data.agent_id) this.myAgentId = data.agent_id;\n this.emit(\"connected\", data);\n resolve();\n } else if (data.type === \"negotiation_message\") {\n const msg = data.message ?? {\n id: data.id,\n from_agent_id: data.from_agent_id,\n type: data.message_type ?? data.type,\n content: data.content,\n terms: data.terms,\n is_catchup: data.is_catchup,\n created_at: data.created_at,\n };\n\n const msgId = msg.id;\n if (msgId && this.seenMessageIds.has(msgId)) return;\n if (msgId) this.seenMessageIds.add(msgId);\n\n if (msg.from_agent_id === this.myAgentId) return;\n\n this.emit(\"incoming_message\", msg);\n } else if (data.type === \"negotiation_started\") {\n this.emit(\"negotiation_started\", data);\n } else if (data.type === \"negotiation_ended\") {\n this.emit(\"ended\", data);\n this.close();\n } else if (data.type === \"message_ack\") {\n if (data.message?.id) this.seenMessageIds.add(data.message.id);\n this.emit(\"message_ack\", data);\n } else if (data.type === \"agent_thinking\") {\n this.emit(\"agent_thinking\", data);\n } else if (data.type === \"error\") {\n this.emit(\"error\", new Error(data.error));\n }\n } catch {}\n });\n\n ws.on(\"error\", (err) => {\n if (ws.readyState === WebSocket.CONNECTING) {\n reject(err);\n }\n });\n\n ws.on(\"close\", () => {\n this.emit(\"disconnected\");\n });\n });\n }\n\n send(msg: { type: string; content?: string; terms?: object }): void {\n if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n sendThinking(): void {\n this.send({ type: \"thinking\" });\n }\n\n close(): void {\n if (this.ws) {\n try { this.ws.close(); } catch {}\n this.ws = null;\n }\n }\n}\n","import { LobbyClient } from \"./connector-lobby.js\";\nimport { NegotiationSession } from \"./connector-session.js\";\nimport { parseResponse } from \"./connector-parser.js\";\nimport {\n setActiveSession,\n removeActiveSession,\n} from \"./bouncer-channel.js\";\nimport type { BouncerPluginConfig, NegotiationState } from \"./types.js\";\nimport type {\n OpenClawPluginService,\n OpenClawPluginServiceContext,\n PluginLogger,\n PluginRuntime,\n} from \"./sdk-types.js\";\n\nlet lobbyClient: LobbyClient | null = null;\n\nexport function getLobbyClient(): LobbyClient | null {\n return lobbyClient;\n}\nlet activeNegotiation: NegotiationState | null = null;\nlet pluginRuntime: PluginRuntime | null = null;\n\nfunction resolveAgentFromBindings(cfg: any, channel: string): string | null {\n const bindings = cfg?.bindings;\n if (!Array.isArray(bindings)) return null;\n for (const b of bindings) {\n if (b?.match?.channel === channel && b?.agentId) {\n return b.agentId;\n }\n }\n return null;\n}\n\nlet serviceStarted = false;\n\nexport function setRuntime(rt: PluginRuntime): void {\n pluginRuntime = rt;\n}\n\nexport async function ensureServiceStarted(log: PluginLogger): Promise<void> {\n if (serviceStarted || lobbyClient) return;\n if (!pluginRuntime) return;\n\n try {\n const cfg = await pluginRuntime.config.loadConfig();\n const bouncerCfg = cfg?.channels?.bouncer as Record<string, unknown> | undefined;\n if (!bouncerCfg?.role) return;\n\n const config: BouncerPluginConfig = {\n role: bouncerCfg.role as \"bouncer\" | \"brand\",\n apiUrl: bouncerCfg.apiUrl as string,\n apiKey: bouncerCfg.apiKey as string,\n agentId: bouncerCfg.agentId as string,\n autoAccept: (bouncerCfg.autoAccept as boolean) ?? true,\n llmTimeoutMs: (bouncerCfg.llmTimeoutMs as number) ?? 25_000,\n };\n\n serviceStarted = true;\n log.info(`[bouncer] Self-starting service — role: ${config.role}, agent: ${config.agentId}`);\n\n await startLobby(config, log);\n } catch (err: any) {\n log.error(`[bouncer] Self-start failed: ${err.message}`);\n serviceStarted = false;\n }\n}\n\nexport function getActiveNegotiation(): NegotiationState | null {\n return activeNegotiation;\n}\n\nexport function createBouncerService(config: BouncerPluginConfig): OpenClawPluginService {\n return {\n id: \"bouncer-negotiation-service\",\n\n async start(ctx: OpenClawPluginServiceContext) {\n const log = ctx.logger;\n\n await startLobby(config, log);\n },\n\n async stop() {\n if (lobbyClient) {\n lobbyClient.close();\n lobbyClient = null;\n }\n if (activeNegotiation) {\n removeActiveSession(activeNegotiation.negotiationId);\n activeNegotiation = null;\n }\n },\n };\n}\n\nasync function startLobby(config: BouncerPluginConfig, log: PluginLogger): Promise<void> {\n const wsBase = config.apiUrl.replace(/^http/, \"ws\");\n\n lobbyClient = new LobbyClient({\n apiUrl: wsBase,\n token: config.apiKey,\n agentId: config.agentId,\n reconnectBaseMs: 1_000,\n reconnectMaxMs: 30_000,\n });\n\n lobbyClient.on(\"connected\", () => {\n log.info(`[bouncer] Lobby connected (${config.role} mode).`);\n });\n\n if (config.role === \"bouncer\") {\n lobbyClient.on(\"negotiation_requested\", (data: any) => {\n handleNegotiationRequested(data, config, wsBase, log);\n });\n }\n\n lobbyClient.on(\"heartbeat\", () => {\n log.debug(\"[bouncer] Lobby heartbeat\");\n });\n\n try {\n await lobbyClient.connect();\n } catch (err: any) {\n log.error(`[bouncer] Lobby connection failed: ${err.message}`);\n throw err;\n }\n}\n\nasync function handleNegotiationRequested(\n data: any,\n config: BouncerPluginConfig,\n wsBase: string,\n log: PluginLogger,\n): Promise<void> {\n const negId = data.negotiation_id;\n\n if (activeNegotiation) {\n log.info(`[bouncer] Declining ${negId} — already in negotiation ${activeNegotiation.negotiationId}`);\n lobbyClient?.decline(negId, \"busy — already in a negotiation\");\n return;\n }\n\n if (!config.autoAccept) {\n log.info(`[bouncer] Negotiation ${negId} requested but autoAccept is off. Declining.`);\n lobbyClient?.decline(negId, \"auto-accept disabled\");\n return;\n }\n\n log.info(`[bouncer] Accepting negotiation ${negId}`);\n lobbyClient?.accept(negId);\n\n activeNegotiation = {\n negotiationId: negId,\n status: \"pending\",\n otherAgentId: data.brand?.id,\n offerContext: data.offer\n ? { product: data.offer.product, terms: data.offer.terms, opening_message: data.offer.opening_message }\n : undefined,\n };\n\n await connectToNegotiationRoom(negId, config, wsBase, log);\n}\n\nexport async function connectToNegotiationRoom(\n negotiationId: string,\n config: BouncerPluginConfig,\n wsBase: string,\n log: PluginLogger,\n): Promise<void> {\n const session = new NegotiationSession({\n apiUrl: wsBase,\n token: config.apiKey,\n negotiationId,\n agentId: config.agentId,\n });\n\n session.on(\"connected\", () => {\n log.info(`[bouncer] Connected to negotiation room ${negotiationId}`);\n });\n\n session.on(\"negotiation_started\", (data: any) => {\n log.info(`[bouncer] Negotiation ${negotiationId} started`);\n if (activeNegotiation) {\n activeNegotiation.status = \"live\";\n if (data.queue_context) {\n (activeNegotiation as any).queueContext = data.queue_context;\n }\n if (data.relationship) {\n (activeNegotiation as any).relationship = data.relationship;\n }\n }\n });\n\n let dispatching = false;\n session.on(\"incoming_message\", (msg: any) => {\n if (dispatching) {\n log.debug(`[bouncer] Skipping message while dispatch in progress`);\n return;\n }\n log.info(`[bouncer] Message from ${msg.from_agent_id}: ${(msg.content ?? \"\").slice(0, 80)}...`);\n dispatching = true;\n dispatchToAgent(negotiationId, msg, config, session, log).finally(() => {\n dispatching = false;\n });\n });\n\n session.on(\"agent_thinking\", () => {\n log.debug(`[bouncer] Other agent thinking in ${negotiationId}`);\n });\n\n session.on(\"ended\", (data: any) => {\n log.info(`[bouncer] Negotiation ${negotiationId} ended: ${data?.status ?? \"unknown\"}`);\n removeActiveSession(negotiationId);\n activeNegotiation = null;\n });\n\n session.on(\"disconnected\", () => {\n log.warn(`[bouncer] Disconnected from negotiation ${negotiationId}`);\n });\n\n setActiveSession(negotiationId, session);\n\n try {\n await session.connect();\n } catch (err: any) {\n log.error(`[bouncer] Failed to connect to negotiation ${negotiationId}: ${err.message}`);\n removeActiveSession(negotiationId);\n activeNegotiation = null;\n }\n}\n\nasync function dispatchToAgent(\n negotiationId: string,\n msg: any,\n config: BouncerPluginConfig,\n session: NegotiationSession,\n log: PluginLogger,\n): Promise<void> {\n if (!pluginRuntime) {\n log.error(\"[bouncer] Runtime not set — cannot dispatch inbound message to agent\");\n return;\n }\n\n const rt = pluginRuntime;\n\n try {\n const currentCfg = await rt.config.loadConfig();\n\n const resolvedAgent = resolveAgentFromBindings(currentCfg, \"bouncer\");\n if (resolvedAgent) {\n log.info(`[bouncer] Routing negotiation ${negotiationId} to agent: ${resolvedAgent}`);\n }\n\n const msgCtx = rt.channel.reply.finalizeInboundContext({\n Body: msg.content ?? \"\",\n RawBody: msg.content ?? \"\",\n CommandBody: msg.content ?? \"\",\n From: `bouncer:${msg.from_agent_id}`,\n To: `bouncer:${config.agentId}`,\n SessionKey: `bouncer:${negotiationId}`,\n AccountId: config.agentId,\n OriginatingChannel: \"bouncer\",\n OriginatingTo: `bouncer:${msg.from_agent_id}`,\n ChatType: \"direct\",\n SenderName: msg.from_agent_id,\n SenderId: msg.from_agent_id,\n Provider: \"bouncer\",\n Surface: \"bouncer\",\n ConversationLabel: `Negotiation ${negotiationId}`,\n Timestamp: Date.now(),\n CommandAuthorized: true,\n ...(resolvedAgent ? { AgentId: resolvedAgent, TargetAgentId: resolvedAgent, ResolvedAgentId: resolvedAgent } : {}),\n });\n\n await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({\n ctx: msgCtx,\n cfg: currentCfg,\n dispatcherOptions: {\n deliver: async (payload) => {\n const text = payload?.text ?? payload?.body;\n if (!text) return;\n\n const parsed = parseResponse(text);\n session.sendThinking();\n await new Promise((r) => setTimeout(r, 200));\n session.send(parsed);\n log.info(`[bouncer] Agent replied (${parsed.type}): ${parsed.content.slice(0, 80)}...`);\n },\n onReplyStart: () => {\n session.sendThinking();\n },\n },\n });\n } catch (err: any) {\n log.error(`[bouncer] Dispatch failed for negotiation ${negotiationId}: ${err.message}`);\n }\n}\n","export interface BouncerPluginConfig {\n role: \"bouncer\" | \"brand\";\n apiUrl: string;\n apiKey: string;\n agentId: string;\n autoAccept?: boolean;\n llmTimeoutMs?: number;\n}\n\nexport interface NegotiationState {\n negotiationId: string;\n status: \"pending\" | \"live\" | \"ended\";\n otherAgentId?: string;\n offerContext?: {\n product: string;\n terms: string;\n opening_message: string;\n };\n}\n\nexport interface InboundNegotiationMessage {\n negotiationId: string;\n fromAgentId: string;\n type: string;\n content: string;\n terms?: Record<string, unknown>;\n timestamp: string;\n}\n\nexport function resolveConfig(pluginConfig: Record<string, unknown> | undefined): BouncerPluginConfig {\n if (!pluginConfig) throw new Error(\"Bouncer plugin config is required\");\n\n const role = pluginConfig.role as string;\n if (role !== \"bouncer\" && role !== \"brand\") {\n throw new Error(`Invalid role \"${role}\" — must be \"bouncer\" or \"brand\"`);\n }\n\n return {\n role,\n apiUrl: pluginConfig.apiUrl as string,\n apiKey: pluginConfig.apiKey as string,\n agentId: pluginConfig.agentId as string,\n autoAccept: (pluginConfig.autoAccept as boolean) ?? true,\n llmTimeoutMs: (pluginConfig.llmTimeoutMs as number) ?? 25_000,\n };\n}\n\nexport function wsUrl(apiUrl: string): string {\n return apiUrl.replace(/^http/, \"ws\");\n}\n","import type { AgentTool, PluginLogger } from \"./sdk-types.js\";\nimport type { BouncerPluginConfig } from \"./types.js\";\nimport { connectToNegotiationRoom, getLobbyClient } from \"./service.js\";\nimport { wsUrl } from \"./types.js\";\n\nexport function createBrandTools(config: BouncerPluginConfig, logger: PluginLogger): AgentTool[] {\n if (config.role !== \"brand\") return [];\n\n const bouncerBrowse: AgentTool = {\n name: \"bouncer_browse\",\n description:\n \"Browse available Bouncer agents in the directory. Returns a list of bouncers \" +\n \"with their name, listing, tags, and reputation score. Use this to find a \" +\n \"bouncer to target with an offer.\",\n parameters: {\n type: \"object\",\n properties: {\n limit: {\n type: \"number\",\n description: \"Max results to return (default: 20)\",\n },\n },\n },\n\n async execute(params: Record<string, unknown>): Promise<unknown> {\n const lobby = getLobbyClient();\n if (!lobby) return { error: \"Not connected to lobby\" };\n\n try {\n const result = await lobby.request(\n { type: \"browse_bouncers\", limit: params.limit ?? 20 },\n \"browse_bouncers_result\",\n );\n logger.info(`[bouncer] Browse returned ${result.bouncers?.length ?? 0} bouncers`);\n return result.bouncers;\n } catch (err: any) {\n return { error: `Browse failed: ${err.message}` };\n }\n },\n };\n\n const bouncerProfile: AgentTool = {\n name: \"bouncer_profile\",\n description:\n \"Get the full profile of a specific Bouncer agent, including their listing, \" +\n \"enrichment data, engagement rules, and verified credentials. Use this before \" +\n \"crafting a personalized offer.\",\n parameters: {\n type: \"object\",\n properties: {\n bouncer_id: {\n type: \"string\",\n description: \"The Bouncer agent's UUID\",\n },\n },\n required: [\"bouncer_id\"],\n },\n\n async execute(params: Record<string, unknown>): Promise<unknown> {\n const lobby = getLobbyClient();\n if (!lobby) return { error: \"Not connected to lobby\" };\n\n try {\n const result = await lobby.request(\n { type: \"get_profile\", bouncer_id: params.bouncer_id },\n \"get_profile_result\",\n );\n if (result.error) return { error: result.error };\n return result.profile;\n } catch (err: any) {\n return { error: `Profile fetch failed: ${err.message}` };\n }\n },\n };\n\n const bouncerOffer: AgentTool = {\n name: \"bouncer_offer\",\n description:\n \"Submit an offer to a Bouncer agent and create a negotiation. This sends your \" +\n \"offer and automatically connects you to the negotiation room when the bouncer \" +\n \"accepts. Returns the offer_id and negotiation_id.\",\n parameters: {\n type: \"object\",\n properties: {\n bouncer_id: {\n type: \"string\",\n description: \"The target Bouncer's UUID\",\n },\n product: {\n type: \"string\",\n description: \"Product or service being offered\",\n },\n terms: {\n type: \"string\",\n description: \"Deal terms (e.g. '$20 credit + free delivery')\",\n },\n opening_message: {\n type: \"string\",\n description: \"Personalized opening message to the Bouncer\",\n },\n priority_bid: {\n type: \"number\",\n description: \"Priority bid in USDC (e.g. 2.0)\",\n },\n arena: {\n type: \"boolean\",\n description: \"Practice mode — no real USDC (default: false)\",\n },\n },\n required: [\"bouncer_id\", \"product\", \"terms\", \"opening_message\", \"priority_bid\"],\n },\n\n async execute(params: Record<string, unknown>): Promise<unknown> {\n const lobby = getLobbyClient();\n if (!lobby) return { error: \"Not connected to lobby\" };\n\n const bouncerId = params.bouncer_id as string;\n\n try {\n logger.info(`[bouncer] Submitting offer to ${bouncerId}`);\n\n const offerResult = await lobby.request(\n {\n type: \"submit_offer\",\n bouncer_id: bouncerId,\n product: params.product,\n terms: params.terms,\n opening_message: params.opening_message,\n priority_bid: params.priority_bid,\n arena: params.arena ?? false,\n },\n \"submit_offer_result\",\n );\n\n if (offerResult.error) {\n return { error: `Offer submission failed: ${offerResult.error}` };\n }\n\n const offerId = offerResult.offer_id;\n logger.info(`[bouncer] Offer submitted: ${offerId}`);\n\n logger.info(`[bouncer] Creating negotiation for offer ${offerId}`);\n const negResult = await lobby.request(\n { type: \"create_negotiation\", offer_id: offerId, bouncer_id: bouncerId },\n \"create_negotiation_result\",\n );\n\n if (negResult.error) {\n return { error: `Negotiation creation failed: ${negResult.error}`, offer_id: offerId };\n }\n\n const negotiationId = negResult.negotiation_id;\n logger.info(`[bouncer] Negotiation created: ${negotiationId}`);\n\n logger.info(`[bouncer] Waiting for bouncer to accept (via WS)...`);\n const accepted = await waitForAcceptanceWS(negotiationId, 120_000);\n\n if (!accepted) {\n return { status: \"declined_or_timeout\", offer_id: offerId, negotiation_id: negotiationId };\n }\n\n logger.info(`[bouncer] Accepted! Connecting to negotiation room...`);\n await connectToNegotiationRoom(negotiationId, config, wsUrl(config.apiUrl), logger);\n\n return {\n status: \"connected\",\n offer_id: offerId,\n negotiation_id: negotiationId,\n message: \"Connected to negotiation room. Incoming messages will appear as chat messages.\",\n };\n } catch (err: any) {\n return { error: `Offer flow failed: ${err.message}` };\n }\n },\n };\n\n return [bouncerBrowse, bouncerProfile, bouncerOffer];\n}\n\nfunction waitForAcceptanceWS(negotiationId: string, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const lobby = getLobbyClient();\n if (!lobby) {\n resolve(false);\n return;\n }\n\n const timer = setTimeout(() => {\n cleanup();\n resolve(false);\n }, timeoutMs);\n\n const onAccepted = (data: any) => {\n if (data.negotiation_id === negotiationId) {\n cleanup();\n resolve(true);\n }\n };\n\n const onDeclined = (data: any) => {\n if (data.negotiation_id === negotiationId) {\n cleanup();\n resolve(false);\n }\n };\n\n const cleanup = () => {\n clearTimeout(timer);\n lobby.removeListener(\"negotiation_accepted\", onAccepted);\n lobby.removeListener(\"negotiation_declined\", onDeclined);\n };\n\n lobby.on(\"negotiation_accepted\", onAccepted);\n lobby.on(\"negotiation_declined\", onDeclined);\n });\n}\n","import { definePluginEntry } from \"./src/sdk-types.js\";\nimport { createBouncerChannel } from \"./src/bouncer-channel.js\";\nimport { createBouncerService, setRuntime, ensureServiceStarted } from \"./src/service.js\";\nimport { createBrandTools } from \"./src/tools.js\";\nimport { resolveConfig } from \"./src/types.js\";\nimport type { OpenClawPluginApi, PluginLogger } from \"./src/sdk-types.js\";\n\nlet pluginLogger: PluginLogger = {\n info: console.log,\n warn: console.warn,\n error: console.error,\n debug: () => {},\n};\n\nconst channel = createBouncerChannel(pluginLogger);\n\nexport default definePluginEntry({\n id: \"bouncer\",\n name: \"Bouncer Protocol\",\n description: \"Agent-to-agent negotiation channel via the Bouncer Protocol. \" +\n \"Handles WebSocket lifecycle, heartbeats, echo filtering, and protocol \" +\n \"message formatting so the LLM only deals with plain text.\",\n\n register(api: OpenClawPluginApi) {\n const logger = api.logger ?? pluginLogger;\n pluginLogger = logger;\n\n api.registerChannel({ plugin: channel });\n setRuntime(api.runtime);\n\n if (api.runtime.logging) {\n const child = api.runtime.logging.getChildLogger(\"bouncer\");\n if (child) pluginLogger = child;\n }\n\n const bouncerConfig = resolveChannelConfig(api);\n if (!bouncerConfig) {\n logger.warn(\"[bouncer] No bouncer channel config found — will self-start from runtime config.\");\n setTimeout(() => {\n ensureServiceStartedWithTools(api, pluginLogger).catch((err: any) => {\n pluginLogger.error(`[bouncer] Deferred start failed: ${err.message}`);\n });\n }, 2_000);\n return;\n }\n\n const config = resolveConfig(bouncerConfig);\n logger.info(`[bouncer] Registering plugin — role: ${config.role}, agent: ${config.agentId}`);\n\n api.registerService(createBouncerService(config));\n\n const tools = createBrandTools(config, logger);\n for (const tool of tools) {\n api.registerTool(tool, { name: tool.name });\n }\n\n logger.info(`[bouncer] Plugin registered. Role: ${config.role}`);\n if (config.role === \"bouncer\") {\n logger.info(\"[bouncer] Lobby service will start on gateway boot.\");\n } else {\n logger.info(\"[bouncer] Brand tools registered: bouncer_browse, bouncer_profile, bouncer_offer\");\n }\n },\n});\n\nasync function ensureServiceStartedWithTools(api: OpenClawPluginApi, log: PluginLogger): Promise<void> {\n try {\n const cfg = await api.runtime.config.loadConfig();\n const bouncerCfg = cfg?.channels?.bouncer as Record<string, unknown> | undefined;\n if (!bouncerCfg?.role) {\n log.warn(\"[bouncer] Deferred start: no channels.bouncer.role found in runtime config.\");\n return;\n }\n\n const config = resolveConfig(bouncerCfg);\n log.info(`[bouncer] Deferred registration — role: ${config.role}, agent: ${config.agentId}`);\n\n api.registerService(createBouncerService(config));\n\n const tools = createBrandTools(config, log);\n for (const tool of tools) {\n api.registerTool(tool, { name: tool.name });\n }\n\n if (config.role === \"bouncer\") {\n log.info(\"[bouncer] Lobby service registered via deferred start.\");\n } else {\n log.info(\"[bouncer] Brand tools registered via deferred start: bouncer_browse, bouncer_profile, bouncer_offer\");\n }\n\n await ensureServiceStarted(log);\n } catch (err: any) {\n log.error(`[bouncer] Deferred start failed: ${err.message}`);\n }\n}\n\nfunction resolveChannelConfig(api: OpenClawPluginApi): Record<string, unknown> | null {\n const fromChannels = api.config?.channels?.bouncer as Record<string, unknown> | undefined;\n if (fromChannels && fromChannels.role) return fromChannels;\n\n if (api.pluginConfig && Object.keys(api.pluginConfig).length > 0) {\n return api.pluginConfig;\n }\n\n if (fromChannels && !fromChannels.role) {\n pluginLogger.warn(\n \"[bouncer] channels.bouncer exists but has no 'role' — check config. \" +\n \"Required: role, apiUrl, apiKey, agentId\"\n );\n }\n\n return null;\n}\n"],"mappings":"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","names":["EventEmitter","WebSocket","channel"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "bouncer",
|
|
3
|
+
"name": "Bouncer Protocol",
|
|
4
|
+
"description": "Agent-to-agent negotiation channel via the Bouncer Protocol",
|
|
5
|
+
"channels": ["bouncer"],
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"role": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["bouncer", "brand"],
|
|
12
|
+
"description": "Agent role — bouncer (receives offers) or brand (sends offers)"
|
|
13
|
+
},
|
|
14
|
+
"apiUrl": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Bouncer API base URL"
|
|
17
|
+
},
|
|
18
|
+
"apiKey": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "Agent JWT (bsk_... for bouncer, brk_... for brand)"
|
|
21
|
+
},
|
|
22
|
+
"agentId": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Bouncer agent UUID"
|
|
25
|
+
},
|
|
26
|
+
"autoAccept": {
|
|
27
|
+
"type": "boolean",
|
|
28
|
+
"description": "Auto-accept incoming negotiation requests (bouncer only)"
|
|
29
|
+
},
|
|
30
|
+
"llmTimeoutMs": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Max time to wait for LLM response before auto-walking (ms)"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": ["role", "apiUrl", "apiKey", "agentId"]
|
|
36
|
+
},
|
|
37
|
+
"enabledByDefault": false
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bouncer-protocol/bouncer",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenClaw channel plugin for the Bouncer Protocol — bridges agent negotiations over WebSocket",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"openclaw": {
|
|
9
|
+
"extensions": ["./dist/index.js"],
|
|
10
|
+
"channel": {
|
|
11
|
+
"id": "bouncer",
|
|
12
|
+
"label": "Bouncer Protocol",
|
|
13
|
+
"selectionLabel": "Bouncer Protocol (Agent Negotiation)",
|
|
14
|
+
"docsPath": "/channels/bouncer",
|
|
15
|
+
"order": 90
|
|
16
|
+
},
|
|
17
|
+
"install": {
|
|
18
|
+
"npmSpec": "@bouncer-protocol/bouncer",
|
|
19
|
+
"localPath": "apps/openclaw-plugin",
|
|
20
|
+
"defaultChoice": "local"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"openclaw.plugin.json",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"dev": "tsup --watch"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"ws": "^8.18.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"openclaw": "*",
|
|
37
|
+
"tsup": "^8.5.1",
|
|
38
|
+
"typescript": "^5.7.0",
|
|
39
|
+
"@types/ws": "^8.5.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"openclaw": ">=2025.0.0"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/camburley/bouncer.git",
|
|
51
|
+
"directory": "apps/openclaw-plugin"
|
|
52
|
+
}
|
|
53
|
+
}
|