@clawcrony/claw-crony 1.2.3 → 1.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/CHANGELOG.md +107 -0
- package/CONFIG.md +341 -0
- package/README.md +154 -23
- package/dist/index.js +610 -112
- package/dist/src/ephemeral-token.d.ts +7 -0
- package/dist/src/ephemeral-token.js +17 -0
- package/dist/src/handshake-crypto.d.ts +3 -0
- package/dist/src/handshake-crypto.js +58 -0
- package/dist/src/history.d.ts +44 -0
- package/dist/src/history.js +119 -0
- package/dist/src/hub-match.d.ts +35 -46
- package/dist/src/hub-match.js +38 -53
- package/dist/src/hub-registration.d.ts +3 -10
- package/dist/src/hub-registration.js +33 -132
- package/dist/src/identity-store.d.ts +4 -0
- package/dist/src/identity-store.js +55 -0
- package/dist/src/types.d.ts +44 -3
- package/openclaw.plugin.json +46 -18
- package/package.json +43 -27
- package/scripts/a2a-diagnose.ps1 +15 -0
- package/scripts/a2a-diagnose.sh +22 -0
- package/scripts/a2a-history.ps1 +21 -0
- package/scripts/a2a-history.sh +9 -0
- package/scripts/a2a-match.ps1 +16 -0
- package/scripts/a2a-match.sh +13 -0
- package/scripts/a2a-peers.ps1 +1 -0
- package/scripts/a2a-peers.sh +4 -0
- package/scripts/a2a-send.ps1 +23 -0
- package/scripts/a2a-send.sh +14 -0
- package/scripts/a2a-update.ps1 +3 -0
- package/scripts/a2a-update.sh +6 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GatewayConfig } from "./types.js";
|
|
2
|
+
export declare const EPHEMERAL_INBOUND_TOKEN_LENGTH = 48;
|
|
3
|
+
export declare function isValidEphemeralInboundToken(value: unknown): value is string;
|
|
4
|
+
export declare function issueEphemeralInboundToken(config: GatewayConfig, _matchId: number, _peerAgentId: number, ttlMs?: number): {
|
|
5
|
+
token: string;
|
|
6
|
+
expiresAt: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
export const EPHEMERAL_INBOUND_TOKEN_LENGTH = 48;
|
|
3
|
+
const EPHEMERAL_INBOUND_TOKEN_PATTERN = /^[0-9a-f]{48}$/;
|
|
4
|
+
export function isValidEphemeralInboundToken(value) {
|
|
5
|
+
return typeof value === "string" && EPHEMERAL_INBOUND_TOKEN_PATTERN.test(value);
|
|
6
|
+
}
|
|
7
|
+
export function issueEphemeralInboundToken(config, _matchId, _peerAgentId, ttlMs = 5 * 60_000) {
|
|
8
|
+
const token = crypto.randomBytes(EPHEMERAL_INBOUND_TOKEN_LENGTH / 2).toString("hex");
|
|
9
|
+
config.security.validTokens.add(token);
|
|
10
|
+
setTimeout(() => {
|
|
11
|
+
config.security.validTokens.delete(token);
|
|
12
|
+
}, ttlMs).unref?.();
|
|
13
|
+
return {
|
|
14
|
+
token,
|
|
15
|
+
expiresAt: new Date(Date.now() + ttlMs).toISOString(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { HandshakePayload, IdentityData } from "./types.js";
|
|
2
|
+
export declare function encryptHandshake(payload: HandshakePayload, recipientPublicKeyPem: string): string;
|
|
3
|
+
export declare function decryptHandshake(ciphertext: string, identity: IdentityData): HandshakePayload;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
function toBase64(value) {
|
|
3
|
+
return Buffer.from(value instanceof Buffer ? value : new Uint8Array(value)).toString("base64");
|
|
4
|
+
}
|
|
5
|
+
function fromBase64(value) {
|
|
6
|
+
return Buffer.from(value, "base64");
|
|
7
|
+
}
|
|
8
|
+
function exportPublicPem(key) {
|
|
9
|
+
return key.export({ format: "pem", type: "spki" }).toString();
|
|
10
|
+
}
|
|
11
|
+
function hkdf(secret, salt, info) {
|
|
12
|
+
return Buffer.from(crypto.hkdfSync("sha256", secret, salt, Buffer.from(info, "utf-8"), 32));
|
|
13
|
+
}
|
|
14
|
+
const HANDSHAKE_INFO = "claw-crony:handshake:v1";
|
|
15
|
+
export function encryptHandshake(payload, recipientPublicKeyPem) {
|
|
16
|
+
const recipientPublicKey = crypto.createPublicKey(recipientPublicKeyPem);
|
|
17
|
+
const ephemeral = crypto.generateKeyPairSync("x25519");
|
|
18
|
+
const sharedSecret = crypto.diffieHellman({
|
|
19
|
+
privateKey: ephemeral.privateKey,
|
|
20
|
+
publicKey: recipientPublicKey,
|
|
21
|
+
});
|
|
22
|
+
const iv = crypto.randomBytes(12);
|
|
23
|
+
const key = hkdf(sharedSecret, iv, HANDSHAKE_INFO);
|
|
24
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
25
|
+
const plaintext = Buffer.from(JSON.stringify(payload), "utf-8");
|
|
26
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
27
|
+
const authTag = cipher.getAuthTag();
|
|
28
|
+
const encrypted = {
|
|
29
|
+
version: 1,
|
|
30
|
+
algorithm: "x25519-aes-256-gcm",
|
|
31
|
+
senderPublicKey: exportPublicPem(ephemeral.publicKey),
|
|
32
|
+
iv: toBase64(iv),
|
|
33
|
+
ciphertext: toBase64(ciphertext),
|
|
34
|
+
authTag: toBase64(authTag),
|
|
35
|
+
};
|
|
36
|
+
return JSON.stringify(encrypted);
|
|
37
|
+
}
|
|
38
|
+
export function decryptHandshake(ciphertext, identity) {
|
|
39
|
+
const envelope = JSON.parse(ciphertext);
|
|
40
|
+
if (envelope.algorithm !== "x25519-aes-256-gcm") {
|
|
41
|
+
throw new Error(`Unsupported handshake algorithm: ${envelope.algorithm}`);
|
|
42
|
+
}
|
|
43
|
+
const privateKey = crypto.createPrivateKey(identity.privateKey);
|
|
44
|
+
const senderPublicKey = crypto.createPublicKey(envelope.senderPublicKey);
|
|
45
|
+
const iv = fromBase64(envelope.iv);
|
|
46
|
+
const sharedSecret = crypto.diffieHellman({
|
|
47
|
+
privateKey,
|
|
48
|
+
publicKey: senderPublicKey,
|
|
49
|
+
});
|
|
50
|
+
const key = hkdf(sharedSecret, iv, HANDSHAKE_INFO);
|
|
51
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
52
|
+
decipher.setAuthTag(fromBase64(envelope.authTag));
|
|
53
|
+
const plaintext = Buffer.concat([
|
|
54
|
+
decipher.update(fromBase64(envelope.ciphertext)),
|
|
55
|
+
decipher.final(),
|
|
56
|
+
]);
|
|
57
|
+
return JSON.parse(plaintext.toString("utf-8"));
|
|
58
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type RequestHistoryType = "match.created" | "match.failed" | "handshake.offer_sent" | "handshake.offer_received" | "handshake.answer_sent" | "handshake.answer_received" | "handshake.failed" | "peer.upserted" | "send.started" | "send.completed" | "send.failed" | "send_file.started" | "send_file.completed" | "send_file.failed" | "task.inbound_completed" | "task.inbound_failed";
|
|
2
|
+
export type RequestHistoryStatus = "started" | "success" | "failure" | "ignored";
|
|
3
|
+
export type RequestHistoryDirection = "inbound" | "outbound" | "local";
|
|
4
|
+
export interface RequestHistoryEntry {
|
|
5
|
+
ts: string;
|
|
6
|
+
type: RequestHistoryType;
|
|
7
|
+
status: RequestHistoryStatus;
|
|
8
|
+
direction?: RequestHistoryDirection;
|
|
9
|
+
matchId?: number;
|
|
10
|
+
messageId?: number;
|
|
11
|
+
peer?: string;
|
|
12
|
+
durationMs?: number;
|
|
13
|
+
detail?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface RequestHistoryFilter {
|
|
16
|
+
count?: number;
|
|
17
|
+
type?: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
direction?: string;
|
|
20
|
+
matchId?: number;
|
|
21
|
+
peer?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RequestHistoryOptions {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
includeEncryptedPayloads: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Append-only request history store for operator-facing troubleshooting.
|
|
29
|
+
* Unlike the audit log, this captures Hub match/handshake milestones and
|
|
30
|
+
* gateway calls. Sensitive fields are redacted before persistence.
|
|
31
|
+
*/
|
|
32
|
+
export declare class RequestHistoryStore {
|
|
33
|
+
private readonly filePath;
|
|
34
|
+
private readonly options;
|
|
35
|
+
private dirEnsured;
|
|
36
|
+
constructor(filePath: string, options?: Partial<RequestHistoryOptions>);
|
|
37
|
+
record(entry: Omit<RequestHistoryEntry, "ts"> & {
|
|
38
|
+
ts?: string;
|
|
39
|
+
}): void;
|
|
40
|
+
tail(filter?: RequestHistoryFilter): Promise<RequestHistoryEntry[]>;
|
|
41
|
+
close(): void;
|
|
42
|
+
private ensureDir;
|
|
43
|
+
private write;
|
|
44
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import readline from "node:readline";
|
|
4
|
+
const SECRET_KEY_NAMES = ["token", "secret", "password", "authorization", "ciphertext"];
|
|
5
|
+
function redactValue(key, value, includeEncryptedPayloads) {
|
|
6
|
+
const normalizedKey = key.toLowerCase();
|
|
7
|
+
if (SECRET_KEY_NAMES.some((name) => normalizedKey.includes(name))) {
|
|
8
|
+
if (normalizedKey.includes("ciphertext") && includeEncryptedPayloads) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
return "[redacted]";
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return value.map((entry) => redactUnknown(entry, includeEncryptedPayloads));
|
|
15
|
+
}
|
|
16
|
+
if (value && typeof value === "object") {
|
|
17
|
+
return redactObject(value, includeEncryptedPayloads);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function redactUnknown(value, includeEncryptedPayloads) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.map((entry) => redactUnknown(entry, includeEncryptedPayloads));
|
|
24
|
+
}
|
|
25
|
+
if (value && typeof value === "object") {
|
|
26
|
+
return redactObject(value, includeEncryptedPayloads);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function redactObject(value, includeEncryptedPayloads) {
|
|
31
|
+
const next = {};
|
|
32
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
33
|
+
next[key] = redactValue(key, entry, includeEncryptedPayloads);
|
|
34
|
+
}
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
function matchesFilter(entry, filter) {
|
|
38
|
+
if (filter.type && entry.type !== filter.type)
|
|
39
|
+
return false;
|
|
40
|
+
if (filter.status && entry.status !== filter.status)
|
|
41
|
+
return false;
|
|
42
|
+
if (filter.direction && entry.direction !== filter.direction)
|
|
43
|
+
return false;
|
|
44
|
+
if (filter.matchId != null && entry.matchId !== filter.matchId)
|
|
45
|
+
return false;
|
|
46
|
+
if (filter.peer && entry.peer !== filter.peer)
|
|
47
|
+
return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Append-only request history store for operator-facing troubleshooting.
|
|
52
|
+
* Unlike the audit log, this captures Hub match/handshake milestones and
|
|
53
|
+
* gateway calls. Sensitive fields are redacted before persistence.
|
|
54
|
+
*/
|
|
55
|
+
export class RequestHistoryStore {
|
|
56
|
+
filePath;
|
|
57
|
+
options;
|
|
58
|
+
dirEnsured = false;
|
|
59
|
+
constructor(filePath, options = {}) {
|
|
60
|
+
this.filePath = filePath;
|
|
61
|
+
this.options = {
|
|
62
|
+
enabled: options.enabled ?? true,
|
|
63
|
+
includeEncryptedPayloads: options.includeEncryptedPayloads ?? false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
record(entry) {
|
|
67
|
+
if (!this.options.enabled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const detail = entry.detail
|
|
71
|
+
? redactObject(entry.detail, this.options.includeEncryptedPayloads)
|
|
72
|
+
: undefined;
|
|
73
|
+
this.write({
|
|
74
|
+
...entry,
|
|
75
|
+
ts: entry.ts ?? new Date().toISOString(),
|
|
76
|
+
...(detail ? { detail } : {}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async tail(filter = {}) {
|
|
80
|
+
if (!fs.existsSync(this.filePath))
|
|
81
|
+
return [];
|
|
82
|
+
const count = Math.min(Math.max(1, Math.floor(filter.count ?? 50)), 500);
|
|
83
|
+
const entries = [];
|
|
84
|
+
const input = fs.createReadStream(this.filePath, { encoding: "utf-8" });
|
|
85
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
86
|
+
for await (const line of rl) {
|
|
87
|
+
if (!line.trim())
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
const entry = JSON.parse(line);
|
|
91
|
+
if (matchesFilter(entry, filter)) {
|
|
92
|
+
entries.push(entry);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Skip malformed lines.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return entries.slice(-count).reverse();
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
// No persistent handles.
|
|
103
|
+
}
|
|
104
|
+
ensureDir() {
|
|
105
|
+
if (this.dirEnsured)
|
|
106
|
+
return;
|
|
107
|
+
this.dirEnsured = true;
|
|
108
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
write(entry) {
|
|
111
|
+
try {
|
|
112
|
+
this.ensureDir();
|
|
113
|
+
fs.appendFileSync(this.filePath, JSON.stringify(entry) + "\n");
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// History is diagnostic only; never crash the gateway.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
package/dist/src/hub-match.d.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hub Match API client for
|
|
3
|
-
*
|
|
4
|
-
* Provides a typed client for the hub's /api/matches endpoints:
|
|
5
|
-
* - POST /api/matches createMatch
|
|
6
|
-
* - GET /api/matches/{id} getMatch
|
|
7
|
-
* - GET /api/matches/pending getPendingMatches
|
|
8
|
-
* - POST /api/matches/{id}/token submitToken
|
|
9
|
-
* - POST /api/matches/{id}/complete completeMatch
|
|
10
|
-
* - POST /api/matches/{id}/cancel cancelMatch
|
|
2
|
+
* Hub Match API client for claw-crony.
|
|
11
3
|
*/
|
|
12
4
|
import type { HubRegistrationData } from "./types.js";
|
|
13
5
|
export interface HubAgentDto {
|
|
14
6
|
id: number;
|
|
15
7
|
name: string;
|
|
16
|
-
address: string;
|
|
17
8
|
skills: string[];
|
|
9
|
+
clientId?: string;
|
|
10
|
+
publicKey?: string;
|
|
11
|
+
presenceStatus?: string;
|
|
18
12
|
}
|
|
19
13
|
export interface HubMatchResult {
|
|
20
14
|
id: number;
|
|
@@ -22,58 +16,53 @@ export interface HubMatchResult {
|
|
|
22
16
|
status: string;
|
|
23
17
|
requester: HubAgentDto | null;
|
|
24
18
|
provider: HubAgentDto | null;
|
|
25
|
-
yourToken
|
|
26
|
-
peerToken
|
|
19
|
+
yourToken?: string | null;
|
|
20
|
+
peerToken?: string | null;
|
|
27
21
|
callerRole?: "requester" | "provider" | "observer" | null;
|
|
28
22
|
requesterTokenSubmitted?: boolean;
|
|
29
23
|
providerTokenSubmitted?: boolean;
|
|
30
24
|
readyForComplete?: boolean;
|
|
25
|
+
requesterHandshakeSent?: boolean;
|
|
26
|
+
providerHandshakeSent?: boolean;
|
|
27
|
+
requesterHandshakeConsumed?: boolean;
|
|
28
|
+
providerHandshakeConsumed?: boolean;
|
|
29
|
+
requesterReady?: boolean;
|
|
30
|
+
providerReady?: boolean;
|
|
31
|
+
readyForConnect?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface HubHandshakeMessage {
|
|
34
|
+
id: number;
|
|
35
|
+
senderAgentId: number;
|
|
36
|
+
receiverAgentId: number;
|
|
37
|
+
messageType: "offer" | "answer";
|
|
38
|
+
ciphertext: string;
|
|
39
|
+
status: string;
|
|
40
|
+
expiresAt: string;
|
|
41
|
+
createdAt?: string;
|
|
42
|
+
consumedAt?: string | null;
|
|
31
43
|
}
|
|
32
44
|
export declare class HubMatchClient {
|
|
33
45
|
private readonly hubUrl;
|
|
34
46
|
private readonly registration;
|
|
35
47
|
constructor(hubUrl: string, registration: HubRegistrationData);
|
|
36
48
|
get agentId(): number;
|
|
37
|
-
get registrationToken(): string;
|
|
38
49
|
static create(): Promise<HubMatchClient>;
|
|
39
50
|
private request;
|
|
40
|
-
/**
|
|
41
|
-
* Create a new match request.
|
|
42
|
-
* @param params.skills - Skills to search for in a provider
|
|
43
|
-
* @param params.description - Optional description of the match request
|
|
44
|
-
* @param params.token - Optional bearer token to include for this agent
|
|
45
|
-
*/
|
|
46
51
|
createMatch(params: {
|
|
47
52
|
skills: string[];
|
|
48
53
|
description?: string;
|
|
49
|
-
token?: string;
|
|
50
54
|
}): Promise<HubMatchResult>;
|
|
51
|
-
/**
|
|
52
|
-
* Get a match result by ID.
|
|
53
|
-
* @param matchId - The match ID
|
|
54
|
-
* @param callerId - Optional agent ID to set callerId query param (affects yourToken)
|
|
55
|
-
*/
|
|
56
55
|
getMatch(matchId: number, callerId?: number): Promise<HubMatchResult>;
|
|
57
|
-
/**
|
|
58
|
-
* Get all pending matches for this agent.
|
|
59
|
-
*/
|
|
60
56
|
getPendingMatches(): Promise<HubMatchResult[]>;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
completeMatch(matchId: number, token: string): Promise<HubMatchResult>;
|
|
73
|
-
/**
|
|
74
|
-
* Cancel a pending or token_exchange match.
|
|
75
|
-
* @param matchId - The match ID
|
|
76
|
-
* @param token - This agent's bearer token (for authorization)
|
|
77
|
-
*/
|
|
78
|
-
cancelMatch(matchId: number, token: string): Promise<HubMatchResult>;
|
|
57
|
+
updatePresence(presenceStatus: "online" | "offline" | "busy", clientVersion?: string): Promise<HubAgentDto>;
|
|
58
|
+
sendHandshakeMessage(matchId: number, params: {
|
|
59
|
+
messageType: "offer" | "answer";
|
|
60
|
+
ciphertext: string;
|
|
61
|
+
expiresAt: string;
|
|
62
|
+
}): Promise<HubHandshakeMessage>;
|
|
63
|
+
getPendingHandshakeMessages(matchId: number): Promise<HubHandshakeMessage[]>;
|
|
64
|
+
consumeHandshakeMessage(matchId: number, messageId: number): Promise<HubHandshakeMessage>;
|
|
65
|
+
markReady(matchId: number): Promise<HubMatchResult>;
|
|
66
|
+
completeMatch(matchId: number): Promise<HubMatchResult>;
|
|
67
|
+
cancelMatch(matchId: number): Promise<HubMatchResult>;
|
|
79
68
|
}
|
package/dist/src/hub-match.js
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hub Match API client for
|
|
3
|
-
*
|
|
4
|
-
* Provides a typed client for the hub's /api/matches endpoints:
|
|
5
|
-
* - POST /api/matches createMatch
|
|
6
|
-
* - GET /api/matches/{id} getMatch
|
|
7
|
-
* - GET /api/matches/pending getPendingMatches
|
|
8
|
-
* - POST /api/matches/{id}/token submitToken
|
|
9
|
-
* - POST /api/matches/{id}/complete completeMatch
|
|
10
|
-
* - POST /api/matches/{id}/cancel cancelMatch
|
|
2
|
+
* Hub Match API client for claw-crony.
|
|
11
3
|
*/
|
|
12
4
|
import { loadRegistration } from "./hub-registration.js";
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// HubMatchClient
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
5
|
export class HubMatchClient {
|
|
17
6
|
hubUrl;
|
|
18
7
|
registration;
|
|
@@ -23,16 +12,12 @@ export class HubMatchClient {
|
|
|
23
12
|
get agentId() {
|
|
24
13
|
return this.registration.agentId;
|
|
25
14
|
}
|
|
26
|
-
get registrationToken() {
|
|
27
|
-
return this.registration.token;
|
|
28
|
-
}
|
|
29
15
|
static async create() {
|
|
30
16
|
const registration = loadRegistration();
|
|
31
17
|
if (!registration) {
|
|
32
18
|
throw new Error("No hub registration found. Run the gateway first to register with the hub.");
|
|
33
19
|
}
|
|
34
|
-
|
|
35
|
-
return new HubMatchClient(configUrl, registration);
|
|
20
|
+
return new HubMatchClient(registration.hubUrl, registration);
|
|
36
21
|
}
|
|
37
22
|
async request(path, options = {}) {
|
|
38
23
|
const url = `${this.hubUrl}${path}`;
|
|
@@ -40,7 +25,6 @@ export class HubMatchClient {
|
|
|
40
25
|
...options,
|
|
41
26
|
headers: {
|
|
42
27
|
"Content-Type": "application/json",
|
|
43
|
-
"Authorization": `Bearer ${this.registration.token}`,
|
|
44
28
|
...(options.headers ?? {}),
|
|
45
29
|
},
|
|
46
30
|
});
|
|
@@ -50,12 +34,6 @@ export class HubMatchClient {
|
|
|
50
34
|
}
|
|
51
35
|
return res.json();
|
|
52
36
|
}
|
|
53
|
-
/**
|
|
54
|
-
* Create a new match request.
|
|
55
|
-
* @param params.skills - Skills to search for in a provider
|
|
56
|
-
* @param params.description - Optional description of the match request
|
|
57
|
-
* @param params.token - Optional bearer token to include for this agent
|
|
58
|
-
*/
|
|
59
37
|
async createMatch(params) {
|
|
60
38
|
return this.request("/api/matches", {
|
|
61
39
|
method: "POST",
|
|
@@ -63,45 +41,57 @@ export class HubMatchClient {
|
|
|
63
41
|
agentId: this.registration.agentId,
|
|
64
42
|
requiredSkills: params.skills,
|
|
65
43
|
description: params.description ?? "",
|
|
66
|
-
token: params.token,
|
|
67
44
|
}),
|
|
68
45
|
});
|
|
69
46
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Get a match result by ID.
|
|
72
|
-
* @param matchId - The match ID
|
|
73
|
-
* @param callerId - Optional agent ID to set callerId query param (affects yourToken)
|
|
74
|
-
*/
|
|
75
47
|
async getMatch(matchId, callerId) {
|
|
76
48
|
const path = callerId != null ? `/api/matches/${matchId}?callerId=${callerId}` : `/api/matches/${matchId}`;
|
|
77
49
|
return this.request(path);
|
|
78
50
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Get all pending matches for this agent.
|
|
81
|
-
*/
|
|
82
51
|
async getPendingMatches() {
|
|
83
52
|
return this.request(`/api/matches/pending?agentId=${this.registration.agentId}`);
|
|
84
53
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
54
|
+
async updatePresence(presenceStatus, clientVersion = "claw-crony/1.3.0") {
|
|
55
|
+
return this.request(`/api/agents/${this.registration.agentId}/presence`, {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
presenceStatus,
|
|
59
|
+
clientVersion,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async sendHandshakeMessage(matchId, params) {
|
|
64
|
+
return this.request(`/api/matches/${matchId}/handshake`, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
agentId: this.registration.agentId,
|
|
68
|
+
messageType: params.messageType,
|
|
69
|
+
ciphertext: params.ciphertext,
|
|
70
|
+
expiresAt: params.expiresAt,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async getPendingHandshakeMessages(matchId) {
|
|
75
|
+
const result = await this.request(`/api/matches/${matchId}/handshake/pending?agentId=${this.registration.agentId}`);
|
|
76
|
+
return result.messages ?? [];
|
|
77
|
+
}
|
|
78
|
+
async consumeHandshakeMessage(matchId, messageId) {
|
|
79
|
+
return this.request(`/api/matches/${matchId}/handshake/${messageId}/consume`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
agentId: this.registration.agentId,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async markReady(matchId) {
|
|
87
|
+
return this.request(`/api/matches/${matchId}/ready`, {
|
|
92
88
|
method: "POST",
|
|
93
89
|
body: JSON.stringify({
|
|
94
90
|
agentId: this.registration.agentId,
|
|
95
|
-
token,
|
|
96
91
|
}),
|
|
97
92
|
});
|
|
98
93
|
}
|
|
99
|
-
|
|
100
|
-
* Mark a match as completed (both parties have submitted tokens).
|
|
101
|
-
* @param matchId - The match ID
|
|
102
|
-
* @param token - This agent's bearer token (for authorization)
|
|
103
|
-
*/
|
|
104
|
-
async completeMatch(matchId, token) {
|
|
94
|
+
async completeMatch(matchId) {
|
|
105
95
|
return this.request(`/api/matches/${matchId}/complete`, {
|
|
106
96
|
method: "POST",
|
|
107
97
|
body: JSON.stringify({
|
|
@@ -109,12 +99,7 @@ export class HubMatchClient {
|
|
|
109
99
|
}),
|
|
110
100
|
});
|
|
111
101
|
}
|
|
112
|
-
|
|
113
|
-
* Cancel a pending or token_exchange match.
|
|
114
|
-
* @param matchId - The match ID
|
|
115
|
-
* @param token - This agent's bearer token (for authorization)
|
|
116
|
-
*/
|
|
117
|
-
async cancelMatch(matchId, token) {
|
|
102
|
+
async cancelMatch(matchId) {
|
|
118
103
|
return this.request(`/api/matches/${matchId}/cancel`, {
|
|
119
104
|
method: "POST",
|
|
120
105
|
body: JSON.stringify({
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hub registration module for
|
|
2
|
+
* Hub registration module for claw-crony.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Registers the local plugin with the hub using client_id + public_key
|
|
5
|
+
* and persists the resulting agent binding locally.
|
|
6
6
|
*/
|
|
7
7
|
import type { GatewayConfig, HubConfig, HubRegistrationData, OpenClawPluginApi, RegistrationConfig } from "./types.js";
|
|
8
8
|
export declare function loadRegistration(configDir?: string): HubRegistrationData | null;
|
|
@@ -13,11 +13,4 @@ export interface HubRegistration {
|
|
|
13
13
|
address: string;
|
|
14
14
|
name: string;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Run the full hub registration flow:
|
|
18
|
-
* 1. Load existing registration (if any)
|
|
19
|
-
* 2. Validate existing token with hub
|
|
20
|
-
* 3. If no valid registration, create new one (handling 409 conflicts)
|
|
21
|
-
* 4. Save registration file atomically
|
|
22
|
-
*/
|
|
23
16
|
export declare function runHubRegistration(api: OpenClawPluginApi, config: GatewayConfig, hubConfig: HubConfig, registrationConfig: RegistrationConfig): Promise<HubRegistration | null>;
|