@artinet/cruiser 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,236 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { createSignedDevicePayload, persistConnectAuth, readStoredAuth, resolveAuthFilePath, resolveOrCreateDeviceIdentity, } from './auth.js';
3
+ export class OpenClawGatewayClient {
4
+ url;
5
+ authToken;
6
+ authPassword;
7
+ device;
8
+ deviceIdentity;
9
+ authFilePath;
10
+ scopes;
11
+ connectTimeoutMs;
12
+ clientId;
13
+ socket;
14
+ connectPromise;
15
+ isConnected = false;
16
+ connectRequestId;
17
+ pendingRequests = new Map();
18
+ constructor({ url, authToken, authPassword, agent, device, scopes, connectTimeoutMs, }) {
19
+ this.url = url;
20
+ this.authFilePath = resolveAuthFilePath(agent);
21
+ const storedAuth = readStoredAuth(this.authFilePath);
22
+ this.authToken = authToken ?? storedAuth?.tokens?.operator?.token;
23
+ this.authPassword = authPassword;
24
+ this.device = device;
25
+ this.deviceIdentity = resolveOrCreateDeviceIdentity({ agent, authFilePath: this.authFilePath });
26
+ const requestedScopes = scopes ?? [];
27
+ const requiredScopes = ['operator.read', 'operator.write'];
28
+ this.scopes = Array.from(new Set([...requiredScopes, ...requestedScopes]));
29
+ this.connectTimeoutMs = connectTimeoutMs;
30
+ this.clientId = uuidv4();
31
+ }
32
+ async ensureConnected() {
33
+ if (this.isConnected && this.socket?.readyState === WebSocket.OPEN) {
34
+ return;
35
+ }
36
+ if (this.connectPromise) {
37
+ return this.connectPromise;
38
+ }
39
+ this.connectPromise = new Promise((resolve, reject) => {
40
+ const socket = new WebSocket(this.url);
41
+ this.socket = socket;
42
+ const connectTimeout = setTimeout(() => {
43
+ reject(new Error('OpenClaw gateway connect timeout'));
44
+ }, this.connectTimeoutMs);
45
+ let challengeFallback;
46
+ function cleanupConnect() {
47
+ clearTimeout(connectTimeout);
48
+ if (challengeFallback) {
49
+ clearTimeout(challengeFallback);
50
+ challengeFallback = undefined;
51
+ }
52
+ }
53
+ socket.onopen = () => {
54
+ challengeFallback = setTimeout(() => {
55
+ this.sendConnectRequest();
56
+ }, 1_000);
57
+ };
58
+ socket.onmessage = (event) => {
59
+ this.handleMessage(event.data, resolve, reject, cleanupConnect);
60
+ };
61
+ socket.onerror = () => {
62
+ cleanupConnect();
63
+ reject(new Error('OpenClaw gateway socket error'));
64
+ };
65
+ socket.onclose = () => {
66
+ this.isConnected = false;
67
+ this.connectPromise = undefined;
68
+ this.connectRequestId = undefined;
69
+ this.socket = undefined;
70
+ this.rejectAllPending(new Error('OpenClaw gateway socket closed'));
71
+ };
72
+ })
73
+ .catch((error) => {
74
+ this.connectPromise = undefined;
75
+ throw error;
76
+ })
77
+ .then(() => undefined);
78
+ return this.connectPromise;
79
+ }
80
+ async requestAgentRun({ message, agentId, sessionKey, timeoutMs, }) {
81
+ await this.ensureConnected();
82
+ const requestId = uuidv4();
83
+ const idempotencyKey = uuidv4();
84
+ const frame = {
85
+ type: 'req',
86
+ id: requestId,
87
+ method: 'agent',
88
+ params: {
89
+ message,
90
+ agentId,
91
+ sessionKey,
92
+ deliver: false,
93
+ idempotencyKey,
94
+ },
95
+ };
96
+ const responsePromise = new Promise((resolve, reject) => {
97
+ this.pendingRequests.set(requestId, {
98
+ expectFinal: true,
99
+ resolve,
100
+ reject,
101
+ });
102
+ });
103
+ this.sendFrame(frame);
104
+ const timeout = new Promise((_, reject) => {
105
+ setTimeout(() => {
106
+ this.pendingRequests.delete(requestId);
107
+ reject(new Error('OpenClaw gateway request timeout'));
108
+ }, timeoutMs);
109
+ });
110
+ const payload = (await Promise.race([responsePromise, timeout]));
111
+ if (payload.status && payload.status !== 'ok') {
112
+ const details = payload.error?.message ?? 'unknown error';
113
+ throw new Error(`OpenClaw agent failed: ${payload.status}: ${details}`);
114
+ }
115
+ return (payload.result ?? payload);
116
+ }
117
+ handleMessage(raw, connectResolve, connectReject, cleanupConnect) {
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(raw);
121
+ }
122
+ catch {
123
+ return;
124
+ }
125
+ const frame = parsed;
126
+ if (frame.type === 'event' && frame.event === 'connect.challenge') {
127
+ this.sendConnectRequest(frame.payload?.nonce);
128
+ return;
129
+ }
130
+ if (frame.type !== 'res' || !frame.id) {
131
+ return;
132
+ }
133
+ const response = parsed;
134
+ if (frame.id === this.connectRequestId) {
135
+ cleanupConnect();
136
+ if (!response.ok) {
137
+ connectReject(new Error(response.error?.message ?? 'OpenClaw gateway connect rejected'));
138
+ return;
139
+ }
140
+ persistConnectAuth({
141
+ authFilePath: this.authFilePath,
142
+ payload: response.payload,
143
+ scopes: this.scopes,
144
+ deviceIdentity: this.deviceIdentity,
145
+ });
146
+ this.isConnected = true;
147
+ connectResolve();
148
+ return;
149
+ }
150
+ const pending = this.pendingRequests.get(frame.id);
151
+ if (!pending) {
152
+ return;
153
+ }
154
+ const payload = response.payload;
155
+ if (pending.expectFinal && payload?.status === 'accepted') {
156
+ return;
157
+ }
158
+ this.pendingRequests.delete(frame.id);
159
+ if (!response.ok) {
160
+ pending.reject(new Error(response.error?.message ?? 'OpenClaw gateway request failed'));
161
+ return;
162
+ }
163
+ pending.resolve(response.payload);
164
+ }
165
+ sendConnectRequest(nonce) {
166
+ if (this.connectRequestId) {
167
+ return;
168
+ }
169
+ const params = {
170
+ minProtocol: 3,
171
+ maxProtocol: 3,
172
+ client: {
173
+ id: 'cli',
174
+ displayName: 'cruiser-openclaw',
175
+ version: '0.1.5',
176
+ platform: 'node',
177
+ mode: 'cli',
178
+ instanceId: this.clientId,
179
+ },
180
+ role: 'operator',
181
+ scopes: this.scopes,
182
+ caps: [],
183
+ commands: [],
184
+ permissions: {},
185
+ locale: 'en-US',
186
+ userAgent: 'artinet-cruiser/openclaw',
187
+ };
188
+ if (this.authToken || this.authPassword) {
189
+ params.auth = {
190
+ ...(this.authToken ? { token: this.authToken } : {}),
191
+ ...(this.authPassword ? { password: this.authPassword } : {}),
192
+ };
193
+ }
194
+ const signedDevice = this.deviceIdentity
195
+ ? createSignedDevicePayload({
196
+ identity: this.deviceIdentity,
197
+ scopes: this.scopes,
198
+ token: this.authToken,
199
+ nonce,
200
+ })
201
+ : undefined;
202
+ if (signedDevice) {
203
+ params.device = signedDevice;
204
+ }
205
+ else if (this.device) {
206
+ const deviceNonce = nonce ?? this.device.nonce;
207
+ params.device = {
208
+ id: this.device.id,
209
+ publicKey: this.device.publicKey,
210
+ signature: this.device.signature,
211
+ signedAt: this.device.signedAt,
212
+ ...(deviceNonce ? { nonce: deviceNonce } : {}),
213
+ };
214
+ }
215
+ const requestId = uuidv4();
216
+ this.connectRequestId = requestId;
217
+ this.sendFrame({
218
+ type: 'req',
219
+ id: requestId,
220
+ method: 'connect',
221
+ params,
222
+ });
223
+ }
224
+ sendFrame(frame) {
225
+ if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
226
+ throw new Error('OpenClaw gateway is not connected');
227
+ }
228
+ this.socket.send(JSON.stringify(frame));
229
+ }
230
+ rejectAllPending(error) {
231
+ for (const pending of this.pendingRequests.values()) {
232
+ pending.reject(error);
233
+ }
234
+ this.pendingRequests.clear();
235
+ }
236
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @fileoverview openclaw → artinet
3
+ *
4
+ * @module @artinet/cruiser/openclaw
5
+ * @description
6
+ * This adapter "docks" OpenClaw Gateway agents into artinet.
7
+ */
8
+ import { Dock, Park } from '../corsair.js';
9
+ import { type OpenClawAgent } from './utils.js';
10
+ /**
11
+ * Configuration options for OpenClaw Gateway WebSocket calls.
12
+ */
13
+ export type OpenClawDockOptions = {
14
+ /**
15
+ * Timeout for WS connect handshake in milliseconds.
16
+ */
17
+ connectTimeoutMs?: number;
18
+ /**
19
+ * Timeout for gateway request completion in milliseconds.
20
+ */
21
+ timeoutMs?: number;
22
+ };
23
+ /**
24
+ * @deprecated Use {@link OpenClawDockOptions} instead.
25
+ */
26
+ export type OpenClawParkOptions = OpenClawDockOptions;
27
+ /**
28
+ * Docks an OpenClaw Gateway agent into artinet.
29
+ *
30
+ * This adapter uses OpenClaw's native Gateway WebSocket protocol.
31
+ *
32
+ * OpenClaw docs:
33
+ * https://docs.openclaw.ai/gateway/protocol
34
+ */
35
+ export declare const dock: Dock<OpenClawAgent, OpenClawDockOptions>;
36
+ /**
37
+ * @deprecated Use {@link dock} instead.
38
+ */
39
+ export declare const park: Park<OpenClawAgent, OpenClawDockOptions>;
40
+ export type { OpenClawAgent } from './utils.js';
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @fileoverview openclaw → artinet
3
+ *
4
+ * @module @artinet/cruiser/openclaw
5
+ * @description
6
+ * This adapter "docks" OpenClaw Gateway agents into artinet.
7
+ */
8
+ import * as sdk from '@artinet/sdk';
9
+ import { OpenClawGatewayClient } from './client.js';
10
+ import { extractOpenClawText, getAgentCard } from './utils.js';
11
+ /**
12
+ * Docks an OpenClaw Gateway agent into artinet.
13
+ *
14
+ * This adapter uses OpenClaw's native Gateway WebSocket protocol.
15
+ *
16
+ * OpenClaw docs:
17
+ * https://docs.openclaw.ai/gateway/protocol
18
+ */
19
+ export const dock = async function dock(agent, card, options) {
20
+ const agentCard = await getAgentCard({ agent, card });
21
+ const gatewayUrl = (agent.gatewayUrl ?? 'ws://127.0.0.1:18789').replace(/\/+$/, '');
22
+ const agentId = agent.agentId ?? 'main';
23
+ const connectTimeoutMs = options?.connectTimeoutMs && options.connectTimeoutMs > 0 ? options.connectTimeoutMs : 10_000;
24
+ const requestTimeoutMs = options?.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : 60_000;
25
+ const gateway = new OpenClawGatewayClient({
26
+ url: gatewayUrl,
27
+ authToken: agent.authToken,
28
+ authPassword: agent.authPassword,
29
+ agent,
30
+ device: agent.device,
31
+ scopes: agent.scopes,
32
+ connectTimeoutMs,
33
+ });
34
+ await gateway.ensureConnected();
35
+ sdk.logger.debug(`OpenClaw[${agentCard.name}]:[card:${JSON.stringify(agentCard)}]`);
36
+ return sdk.cr8(agentCard).from(async function* (context) {
37
+ sdk.logger.debug(`OpenClaw[${agentCard.name}]:[context:${context.contextId}]: starting`);
38
+ const task = await context.getTask();
39
+ const text = sdk.extractTextContent(context.userMessage);
40
+ if (!text || text.trim().length === 0) {
41
+ yield sdk.describe.update.failed({
42
+ taskId: context.taskId,
43
+ contextId: context.contextId,
44
+ message: sdk.describe.message({
45
+ taskId: context.taskId,
46
+ contextId: context.contextId,
47
+ parts: [sdk.describe.part.text('no input text detected')],
48
+ }),
49
+ });
50
+ return;
51
+ }
52
+ try {
53
+ const result = await gateway.requestAgentRun({
54
+ message: text,
55
+ agentId,
56
+ sessionKey: agent.sessionKey ?? context.contextId,
57
+ timeoutMs: requestTimeoutMs,
58
+ });
59
+ const responseText = extractOpenClawText(result);
60
+ const message = sdk.describe.message({
61
+ taskId: context.taskId,
62
+ contextId: context.contextId,
63
+ parts: [sdk.describe.part.text(responseText)],
64
+ });
65
+ yield sdk.describe.update.completed({
66
+ taskId: context.taskId,
67
+ contextId: context.contextId,
68
+ message,
69
+ metadata: {
70
+ ...(task.metadata ?? {}),
71
+ result,
72
+ },
73
+ });
74
+ }
75
+ catch (error) {
76
+ sdk.logger.error('OpenClaw execution failed', error);
77
+ const errorMessage = error instanceof Error ? error.message : String(error);
78
+ yield sdk.describe.update.failed({
79
+ taskId: context.taskId,
80
+ contextId: context.contextId,
81
+ message: sdk.describe.message({
82
+ taskId: context.taskId,
83
+ contextId: context.contextId,
84
+ parts: [sdk.describe.part.text(`error invoking OpenClaw agent: ${errorMessage}`)],
85
+ metadata: {
86
+ ...(task.metadata ?? {}),
87
+ error: errorMessage,
88
+ },
89
+ }),
90
+ });
91
+ }
92
+ });
93
+ };
94
+ /**
95
+ * @deprecated Use {@link dock} instead.
96
+ */
97
+ export const park = dock;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview openclaw → artinet utils
3
+ *
4
+ * @module @artinet/cruiser/openclaw/utils
5
+ * @internal
6
+ */
7
+ import * as sdk from "@artinet/sdk";
8
+ export type OpenClawTool = {
9
+ name: string;
10
+ description?: string;
11
+ };
12
+ export type OpenClawDeviceIdentity = {
13
+ id: string;
14
+ publicKey: string;
15
+ privateKeyPem?: string;
16
+ signature?: string;
17
+ signedAt?: number;
18
+ nonce?: string;
19
+ };
20
+ export type OpenClawAgent = {
21
+ /**
22
+ * A2A card display name.
23
+ */
24
+ name: string;
25
+ /**
26
+ * Gateway base URL, e.g. http://127.0.0.1:18789
27
+ */
28
+ gatewayUrl?: string;
29
+ /**
30
+ * OpenClaw agent id. Defaults to "main".
31
+ */
32
+ agentId?: string;
33
+ /**
34
+ * Gateway auth token (token mode).
35
+ */
36
+ authToken?: string;
37
+ /**
38
+ * Gateway auth password (password mode).
39
+ */
40
+ authPassword?: string;
41
+ /**
42
+ * Optional fixed session key routed by OpenClaw gateway.
43
+ */
44
+ sessionKey?: string;
45
+ /**
46
+ * Optional explicit device identity for strict pairing setups.
47
+ */
48
+ device?: OpenClawDeviceIdentity;
49
+ /**
50
+ * Optional path where cruiser stores generated OpenClaw device auth state.
51
+ * Defaults to ~/artinet-openclaw.auth when auto device auth is enabled.
52
+ */
53
+ authFilePath?: string;
54
+ /**
55
+ * Enables automatic device identity bootstrap + persisted device token usage.
56
+ * Defaults to true.
57
+ */
58
+ autoDeviceAuth?: boolean;
59
+ /**
60
+ * Optional custom scopes passed during gateway connect.
61
+ */
62
+ scopes?: string[];
63
+ description?: string;
64
+ tools?: OpenClawTool[];
65
+ };
66
+ export declare function getAgentCard({ agent, card, }: {
67
+ agent: OpenClawAgent;
68
+ card?: sdk.A2A.AgentCardParams;
69
+ }): Promise<sdk.A2A.AgentCard>;
70
+ export declare function extractOpenClawText(result: OpenClawResult): string;
71
+ export type OpenClawResult = Record<string, unknown>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @fileoverview openclaw → artinet utils
3
+ *
4
+ * @module @artinet/cruiser/openclaw/utils
5
+ * @internal
6
+ */
7
+ import * as sdk from "@artinet/sdk";
8
+ function createSkills(tools) {
9
+ return (tools?.map((tool) => ({
10
+ id: tool.name,
11
+ name: tool.name,
12
+ description: tool.description ?? `Tool: ${tool.name}`,
13
+ tags: ["tool"],
14
+ })) ?? []);
15
+ }
16
+ function createDescription(agent) {
17
+ if (agent.description && agent.description.trim().length > 0) {
18
+ return agent.description.trim();
19
+ }
20
+ return "An OpenClaw Gateway agent connected through @artinet/cruiser";
21
+ }
22
+ export async function getAgentCard({ agent, card, }) {
23
+ return sdk.describe.card({
24
+ name: agent.name,
25
+ ...(typeof card === "string" ? { name: card } : card),
26
+ description: createDescription(agent),
27
+ capabilities: {
28
+ streaming: true,
29
+ pushNotifications: true,
30
+ stateTransitionHistory: false,
31
+ },
32
+ defaultInputModes: ["text"],
33
+ defaultOutputModes: ["text"],
34
+ skills: createSkills(agent.tools),
35
+ });
36
+ }
37
+ function asRecord(value) {
38
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
39
+ return undefined;
40
+ }
41
+ return value;
42
+ }
43
+ export function extractOpenClawText(result) {
44
+ const record = asRecord(result);
45
+ if (!record) {
46
+ return "";
47
+ }
48
+ const payloads = record.payloads;
49
+ if (Array.isArray(payloads)) {
50
+ const text = payloads
51
+ .map((payload) => {
52
+ const entry = asRecord(payload);
53
+ if (typeof entry?.text === "string") {
54
+ return entry.text;
55
+ }
56
+ return "";
57
+ })
58
+ .filter((item) => item.length > 0)
59
+ .join("\n")
60
+ .trim();
61
+ if (text.length > 0) {
62
+ return text;
63
+ }
64
+ }
65
+ const choices = record.choices;
66
+ if (!Array.isArray(choices) || choices.length === 0) {
67
+ return "";
68
+ }
69
+ const firstChoice = asRecord(choices[0]);
70
+ const message = asRecord(firstChoice?.message);
71
+ const content = message?.content;
72
+ if (typeof content === "string") {
73
+ return content;
74
+ }
75
+ if (Array.isArray(content)) {
76
+ return content
77
+ .map((part) => {
78
+ if (typeof part === "string") {
79
+ return part;
80
+ }
81
+ const partRecord = asRecord(part);
82
+ if (typeof partRecord?.text === "string") {
83
+ return partRecord.text;
84
+ }
85
+ return JSON.stringify(part);
86
+ })
87
+ .join("\n");
88
+ }
89
+ return "";
90
+ }