@masons/agent-network 0.5.16 → 0.5.18
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/dist/channel-setup.d.ts +20 -0
- package/dist/channel-setup.d.ts.map +1 -0
- package/dist/channel-setup.js +127 -0
- package/dist/cli-setup.d.ts.map +1 -1
- package/dist/cli-setup.js +7 -138
- package/dist/handoff.d.ts +28 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +150 -0
- package/dist/plugin.d.ts +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +6 -5
- package/dist/tools.d.ts +9 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +130 -33
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/skills/agent-network/SKILL.md +10 -8
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface StartChannelSetupOptions {
|
|
2
|
+
apiHost: string;
|
|
3
|
+
connectorUrl: string;
|
|
4
|
+
idpBaseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ChannelSetupResult {
|
|
7
|
+
status: "pending" | "completed" | "expired" | "failed";
|
|
8
|
+
handoffUrl?: string;
|
|
9
|
+
expiresAt?: number;
|
|
10
|
+
agentHandle?: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function getDefaultChannelSetupOptions(): StartChannelSetupOptions;
|
|
14
|
+
export declare function startOrGetChannelSetup(options: StartChannelSetupOptions): Promise<ChannelSetupResult>;
|
|
15
|
+
export declare function getChannelSetupStatus(): ChannelSetupResult | null;
|
|
16
|
+
export declare function consumeChannelSetupStatus(): ChannelSetupResult | null;
|
|
17
|
+
export declare function _resetChannelSetupForTesting(): void;
|
|
18
|
+
export declare const CHANNEL_SETUP_TTL_MS: number;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=channel-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-setup.d.ts","sourceRoot":"","sources":["../src/channel-setup.ts"],"names":[],"mappings":"AAeA,UAAU,wBAAwB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAgBD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAKD,wBAAgB,6BAA6B,IAAI,wBAAwB,CAMxE;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA8B7B;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,GAAG,IAAI,CAejE;AAED,wBAAgB,yBAAyB,IAAI,kBAAkB,GAAG,IAAI,CAMrE;AA8DD,wBAAgB,4BAA4B,IAAI,IAAI,CAGnD;AAED,eAAO,MAAM,oBAAoB,QAAqB,CAAC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readExistingConnectorUrl, writeCredentials, } from "./config.js";
|
|
2
|
+
import { beginBridgeHandoff, completeBridgeHandoff, DEFAULT_IDP_BASE_URL, HANDOFF_TIMEOUT_MS, SetupFlowError, } from "./handoff.js";
|
|
3
|
+
import { DEFAULT_API_HOST, DEFAULT_CONNECTOR_URL } from "./platform-client.js";
|
|
4
|
+
let pendingSetup = null;
|
|
5
|
+
let lastSetupStatus = null;
|
|
6
|
+
export function getDefaultChannelSetupOptions() {
|
|
7
|
+
return {
|
|
8
|
+
apiHost: DEFAULT_API_HOST,
|
|
9
|
+
connectorUrl: DEFAULT_CONNECTOR_URL,
|
|
10
|
+
idpBaseUrl: DEFAULT_IDP_BASE_URL,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export async function startOrGetChannelSetup(options) {
|
|
14
|
+
const active = getActivePendingSetup();
|
|
15
|
+
if (active) {
|
|
16
|
+
return {
|
|
17
|
+
status: "pending",
|
|
18
|
+
handoffUrl: active.session.handoffUrl,
|
|
19
|
+
expiresAt: active.session.expiresAt,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
lastSetupStatus = null;
|
|
23
|
+
const session = await beginBridgeHandoff({
|
|
24
|
+
apiHost: options.apiHost,
|
|
25
|
+
idpBaseUrl: options.idpBaseUrl,
|
|
26
|
+
});
|
|
27
|
+
const record = {
|
|
28
|
+
session,
|
|
29
|
+
apiHost: options.apiHost,
|
|
30
|
+
connectorUrl: options.connectorUrl,
|
|
31
|
+
idpBaseUrl: options.idpBaseUrl,
|
|
32
|
+
};
|
|
33
|
+
pendingSetup = record;
|
|
34
|
+
void completePendingSetup(record);
|
|
35
|
+
return {
|
|
36
|
+
status: "pending",
|
|
37
|
+
handoffUrl: session.handoffUrl,
|
|
38
|
+
expiresAt: session.expiresAt,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function getChannelSetupStatus() {
|
|
42
|
+
const active = getActivePendingSetup();
|
|
43
|
+
if (active) {
|
|
44
|
+
return {
|
|
45
|
+
status: "pending",
|
|
46
|
+
handoffUrl: active.session.handoffUrl,
|
|
47
|
+
expiresAt: active.session.expiresAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (!lastSetupStatus)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
status: lastSetupStatus.status,
|
|
54
|
+
agentHandle: lastSetupStatus.agentHandle,
|
|
55
|
+
message: lastSetupStatus.message,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function consumeChannelSetupStatus() {
|
|
59
|
+
const status = getChannelSetupStatus();
|
|
60
|
+
if (status?.status !== "pending") {
|
|
61
|
+
lastSetupStatus = null;
|
|
62
|
+
}
|
|
63
|
+
return status;
|
|
64
|
+
}
|
|
65
|
+
function getActivePendingSetup() {
|
|
66
|
+
if (!pendingSetup)
|
|
67
|
+
return null;
|
|
68
|
+
if (Date.now() < pendingSetup.session.expiresAt)
|
|
69
|
+
return pendingSetup;
|
|
70
|
+
lastSetupStatus = {
|
|
71
|
+
status: "expired",
|
|
72
|
+
message: "The previous setup link expired before the browser handoff completed.",
|
|
73
|
+
finishedAt: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
pendingSetup = null;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
async function completePendingSetup(record) {
|
|
79
|
+
try {
|
|
80
|
+
const handoff = await completeBridgeHandoff(record.session);
|
|
81
|
+
if (pendingSetup !== record)
|
|
82
|
+
return;
|
|
83
|
+
const existingConnectorUrl = await readExistingConnectorUrl();
|
|
84
|
+
const creds = {
|
|
85
|
+
connectorUrl: existingConnectorUrl ?? record.connectorUrl,
|
|
86
|
+
token: handoff.token,
|
|
87
|
+
};
|
|
88
|
+
await writeCredentials(creds, record.apiHost, record.idpBaseUrl);
|
|
89
|
+
lastSetupStatus = {
|
|
90
|
+
status: "completed",
|
|
91
|
+
agentHandle: handoff.agent.handle,
|
|
92
|
+
message: `Setup completed for @${handoff.agent.handle}.`,
|
|
93
|
+
finishedAt: Date.now(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (pendingSetup !== record)
|
|
98
|
+
return;
|
|
99
|
+
lastSetupStatus = {
|
|
100
|
+
status: isExpiryError(err) ? "expired" : "failed",
|
|
101
|
+
message: safeSetupErrorMessage(err),
|
|
102
|
+
finishedAt: Date.now(),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
if (pendingSetup === record) {
|
|
107
|
+
pendingSetup = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function isExpiryError(err) {
|
|
112
|
+
if (!(err instanceof SetupFlowError))
|
|
113
|
+
return false;
|
|
114
|
+
return /expired|timed out/i.test(err.message);
|
|
115
|
+
}
|
|
116
|
+
function safeSetupErrorMessage(err) {
|
|
117
|
+
if (err instanceof SetupFlowError)
|
|
118
|
+
return err.message;
|
|
119
|
+
if (err instanceof Error)
|
|
120
|
+
return `Setup failed (${err.message}). Re-run setup.`;
|
|
121
|
+
return "Setup failed. Re-run setup.";
|
|
122
|
+
}
|
|
123
|
+
export function _resetChannelSetupForTesting() {
|
|
124
|
+
pendingSetup = null;
|
|
125
|
+
lastSetupStatus = null;
|
|
126
|
+
}
|
|
127
|
+
export const CHANNEL_SETUP_TTL_MS = HANDOFF_TIMEOUT_MS;
|
package/dist/cli-setup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-setup.d.ts","sourceRoot":"","sources":["../src/cli-setup.ts"],"names":[],"mappings":"AAqFA,UAAU,eAAe;IACvB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAOD,UAAU,gBAAgB;IACxB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,eAAe,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"cli-setup.d.ts","sourceRoot":"","sources":["../src/cli-setup.ts"],"names":[],"mappings":"AAqFA,UAAU,eAAe;IACvB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAOD,UAAU,gBAAgB;IACxB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,eAAe,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+ED,wBAAsB,KAAK,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqDhE"}
|
package/dist/cli-setup.js
CHANGED
|
@@ -1,152 +1,21 @@
|
|
|
1
|
-
import { createDecipheriv, constants as cryptoConstants, generateKeyPairSync, privateDecrypt, } from "node:crypto";
|
|
2
1
|
import { readExistingConnectorUrl, writeCredentials } from "./config.js";
|
|
2
|
+
import { beginBridgeHandoff, completeBridgeHandoff, DEFAULT_IDP_BASE_URL, SetupFlowError, } from "./handoff.js";
|
|
3
3
|
import { DEFAULT_API_HOST, DEFAULT_CONNECTOR_URL, PlatformApiError, } from "./platform-client.js";
|
|
4
|
-
const DEFAULT_IDP_BASE_URL = "https://preview.masons.ai";
|
|
5
|
-
const HANDLE_REGEX = /^[a-z][a-z0-9_-]{2,14}$/;
|
|
6
|
-
const HANDOFF_TIMEOUT_MS = 5 * 60 * 1000;
|
|
7
|
-
const HANDOFF_POLL_INTERVAL_MS = 2000;
|
|
8
|
-
class SetupFlowError extends Error {
|
|
9
|
-
}
|
|
10
4
|
async function serverBridgeHandoffFlow(apiHost, idpBaseUrl, runtime, channelInput) {
|
|
11
|
-
const
|
|
12
|
-
|
|
5
|
+
const session = await beginBridgeHandoff({
|
|
6
|
+
apiHost,
|
|
7
|
+
idpBaseUrl,
|
|
8
|
+
channelInput,
|
|
13
9
|
});
|
|
14
|
-
const cliPubkeySpki = publicKey
|
|
15
|
-
.export({ type: "spki", format: "der" })
|
|
16
|
-
.toString("base64url");
|
|
17
|
-
const apiBase = normalizeHttpBase(apiHost);
|
|
18
|
-
let session_id;
|
|
19
|
-
try {
|
|
20
|
-
const initRes = await fetch(`${apiBase}/v1/cli-handoff/init`, {
|
|
21
|
-
method: "POST",
|
|
22
|
-
headers: { "content-type": "application/json" },
|
|
23
|
-
body: JSON.stringify({ cli_pubkey: cliPubkeySpki }),
|
|
24
|
-
});
|
|
25
|
-
if (!initRes.ok) {
|
|
26
|
-
throw new SetupFlowError(`Setup service rejected init (HTTP ${initRes.status}). Retry in a moment; if it persists, check that ${apiBase} is reachable and not behind a proxy that strips the request body.`);
|
|
27
|
-
}
|
|
28
|
-
const initBody = (await initRes.json());
|
|
29
|
-
if (typeof initBody.session_id !== "string" ||
|
|
30
|
-
initBody.session_id.length === 0) {
|
|
31
|
-
throw new SetupFlowError("Setup service returned a malformed response (missing session_id). Retry; if it persists, this is a server-side bug worth reporting.");
|
|
32
|
-
}
|
|
33
|
-
session_id = initBody.session_id;
|
|
34
|
-
}
|
|
35
|
-
catch (err) {
|
|
36
|
-
if (err instanceof SetupFlowError)
|
|
37
|
-
throw err;
|
|
38
|
-
const message = err instanceof Error ? err.message : "unknown error";
|
|
39
|
-
throw new SetupFlowError(`Could not reach the setup service at ${apiBase} (${message}). Retry in a moment.`);
|
|
40
|
-
}
|
|
41
|
-
const handle = parseHandleFromInput(channelInput);
|
|
42
|
-
const handoffPath = handle
|
|
43
|
-
? `/console/agents/${encodeURIComponent(handle)}/runtime-keys/handoff`
|
|
44
|
-
: `/console/handoff`;
|
|
45
|
-
const handoffUrl = `${idpBaseUrl}${handoffPath}?session=${encodeURIComponent(session_id)}`;
|
|
46
10
|
runtime.writeStdout([
|
|
47
11
|
"",
|
|
48
12
|
"Open this link in your browser to finish setup:",
|
|
49
|
-
` ${handoffUrl}`,
|
|
13
|
+
` ${session.handoffUrl}`,
|
|
50
14
|
"",
|
|
51
15
|
"Waiting for the runtime key to be issued and delivered…",
|
|
52
16
|
"",
|
|
53
17
|
].join("\n"));
|
|
54
|
-
|
|
55
|
-
let payload;
|
|
56
|
-
try {
|
|
57
|
-
payload = decryptBridgeEnvelope(privateKey, envelope);
|
|
58
|
-
}
|
|
59
|
-
catch (err) {
|
|
60
|
-
const message = err instanceof Error ? err.message : "unknown error";
|
|
61
|
-
throw new SetupFlowError(`Handoff received but could not be decrypted (${message}). The link may have been tampered with — re-run setup. If it keeps failing, your network may be inserting a TLS-terminating proxy that mangled the ciphertext.`);
|
|
62
|
-
}
|
|
63
|
-
if (typeof payload.token !== "string" ||
|
|
64
|
-
!payload.token.startsWith("masons_rt_v1_") ||
|
|
65
|
-
typeof payload.agent?.handle !== "string" ||
|
|
66
|
-
typeof payload.agent?.agentId !== "string") {
|
|
67
|
-
throw new SetupFlowError("Handoff payload is missing required fields. Re-run setup; if it keeps happening, this is a server-side bug worth reporting.");
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
token: payload.token,
|
|
71
|
-
agent: {
|
|
72
|
-
agentId: payload.agent.agentId,
|
|
73
|
-
handle: payload.agent.handle,
|
|
74
|
-
name: typeof payload.agent.name === "string"
|
|
75
|
-
? payload.agent.name
|
|
76
|
-
: payload.agent.handle,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
async function pollForCompletion(apiBase, sessionId) {
|
|
81
|
-
const start = Date.now();
|
|
82
|
-
while (Date.now() - start < HANDOFF_TIMEOUT_MS) {
|
|
83
|
-
let res;
|
|
84
|
-
try {
|
|
85
|
-
res = await fetch(`${apiBase}/v1/cli-handoff/${encodeURIComponent(sessionId)}/poll`, { method: "GET" });
|
|
86
|
-
}
|
|
87
|
-
catch (_err) {
|
|
88
|
-
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (res.status === 404) {
|
|
92
|
-
throw new SetupFlowError("Handoff session expired before the runtime key was delivered. Re-run setup.");
|
|
93
|
-
}
|
|
94
|
-
if (!res.ok) {
|
|
95
|
-
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const body = (await res.json());
|
|
99
|
-
if (body.status === "completed" && body.payload) {
|
|
100
|
-
return body.payload;
|
|
101
|
-
}
|
|
102
|
-
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
103
|
-
}
|
|
104
|
-
throw new SetupFlowError("Handoff timed out before the runtime key was delivered. Re-run setup; if you're behind a proxy that blocks long-running fetches, retry on a different network.");
|
|
105
|
-
}
|
|
106
|
-
function decryptBridgeEnvelope(privateKey, envelope) {
|
|
107
|
-
const wrappedKey = Buffer.from(envelope.wrapped_key, "base64url");
|
|
108
|
-
const iv = Buffer.from(envelope.iv, "base64url");
|
|
109
|
-
const fullCiphertext = Buffer.from(envelope.ciphertext, "base64url");
|
|
110
|
-
if (fullCiphertext.length < 17) {
|
|
111
|
-
throw new Error("ciphertext shorter than auth tag");
|
|
112
|
-
}
|
|
113
|
-
const tagOffset = fullCiphertext.length - 16;
|
|
114
|
-
const ciphertext = fullCiphertext.subarray(0, tagOffset);
|
|
115
|
-
const authTag = fullCiphertext.subarray(tagOffset);
|
|
116
|
-
const aesKey = privateDecrypt({
|
|
117
|
-
key: privateKey,
|
|
118
|
-
padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING,
|
|
119
|
-
oaepHash: "sha256",
|
|
120
|
-
}, wrappedKey);
|
|
121
|
-
if (aesKey.length !== 32) {
|
|
122
|
-
throw new Error(`unexpected AES key length: ${aesKey.length}`);
|
|
123
|
-
}
|
|
124
|
-
const decipher = createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
125
|
-
decipher.setAuthTag(authTag);
|
|
126
|
-
const plaintext = Buffer.concat([
|
|
127
|
-
decipher.update(ciphertext),
|
|
128
|
-
decipher.final(),
|
|
129
|
-
]);
|
|
130
|
-
return JSON.parse(plaintext.toString("utf-8"));
|
|
131
|
-
}
|
|
132
|
-
function normalizeHttpBase(apiHost) {
|
|
133
|
-
const trimmed = apiHost.replace(/\/+$/, "");
|
|
134
|
-
if (/^https?:\/\//.test(trimmed))
|
|
135
|
-
return trimmed;
|
|
136
|
-
return `https://${trimmed}`;
|
|
137
|
-
}
|
|
138
|
-
function parseHandleFromInput(input) {
|
|
139
|
-
if (typeof input !== "string")
|
|
140
|
-
return undefined;
|
|
141
|
-
const trimmed = input.trim().toLowerCase();
|
|
142
|
-
if (!HANDLE_REGEX.test(trimmed))
|
|
143
|
-
return undefined;
|
|
144
|
-
return trimmed;
|
|
145
|
-
}
|
|
146
|
-
function sleep(ms) {
|
|
147
|
-
return new Promise((resolve) => {
|
|
148
|
-
setTimeout(resolve, ms);
|
|
149
|
-
});
|
|
18
|
+
return completeBridgeHandoff(session);
|
|
150
19
|
}
|
|
151
20
|
export async function login(ctx) {
|
|
152
21
|
const apiHost = typeof ctx.cfg.apiHost === "string" ? ctx.cfg.apiHost : DEFAULT_API_HOST;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type KeyObject } from "node:crypto";
|
|
2
|
+
export declare const DEFAULT_IDP_BASE_URL = "https://preview.masons.ai";
|
|
3
|
+
export declare const HANDOFF_TIMEOUT_MS: number;
|
|
4
|
+
export declare class SetupFlowError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export interface BridgeHandoffResult {
|
|
7
|
+
token: string;
|
|
8
|
+
agent: {
|
|
9
|
+
agentId: string;
|
|
10
|
+
handle: string;
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface BridgeHandoffSession {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
handoffUrl: string;
|
|
17
|
+
apiBase: string;
|
|
18
|
+
privateKey: KeyObject;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
}
|
|
21
|
+
export interface BeginBridgeHandoffOptions {
|
|
22
|
+
apiHost: string;
|
|
23
|
+
idpBaseUrl: string;
|
|
24
|
+
channelInput?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function beginBridgeHandoff(options: BeginBridgeHandoffOptions): Promise<BridgeHandoffSession>;
|
|
27
|
+
export declare function completeBridgeHandoff(session: BridgeHandoffSession): Promise<BridgeHandoffResult>;
|
|
28
|
+
//# sourceMappingURL=handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.d.ts","sourceRoot":"","sources":["../src/handoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EAEf,MAAM,aAAa,CAAC;AAGrB,eAAO,MAAM,oBAAoB,8BAA8B,CAAC;AAOhE,eAAO,MAAM,kBAAkB,QAAgB,CAAC;AAKhD,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAY5C,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1D;AAQD,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAMD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,oBAAoB,CAAC,CAoD/B;AAKD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,mBAAmB,CAAC,CAmC9B"}
|
package/dist/handoff.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createDecipheriv, constants as cryptoConstants, generateKeyPairSync, privateDecrypt, } from "node:crypto";
|
|
2
|
+
export const DEFAULT_IDP_BASE_URL = "https://preview.masons.ai";
|
|
3
|
+
const HANDLE_REGEX = /^[a-z][a-z0-9_-]{2,14}$/;
|
|
4
|
+
export const HANDOFF_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5
|
+
const HANDOFF_POLL_INTERVAL_MS = 2000;
|
|
6
|
+
export class SetupFlowError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export async function beginBridgeHandoff(options) {
|
|
9
|
+
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
|
10
|
+
modulusLength: 2048,
|
|
11
|
+
});
|
|
12
|
+
const cliPubkeySpki = publicKey
|
|
13
|
+
.export({ type: "spki", format: "der" })
|
|
14
|
+
.toString("base64url");
|
|
15
|
+
const apiBase = normalizeHttpBase(options.apiHost);
|
|
16
|
+
let sessionId;
|
|
17
|
+
try {
|
|
18
|
+
const initRes = await fetch(`${apiBase}/v1/cli-handoff/init`, {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify({ cli_pubkey: cliPubkeySpki }),
|
|
22
|
+
});
|
|
23
|
+
if (!initRes.ok) {
|
|
24
|
+
throw new SetupFlowError(`Setup service rejected init (HTTP ${initRes.status}). Retry in a moment; if it persists, check that ${apiBase} is reachable and not behind a proxy that strips the request body.`);
|
|
25
|
+
}
|
|
26
|
+
const initBody = (await initRes.json());
|
|
27
|
+
if (typeof initBody.session_id !== "string" ||
|
|
28
|
+
initBody.session_id.length === 0) {
|
|
29
|
+
throw new SetupFlowError("Setup service returned a malformed response (missing session_id). Retry; if it persists, this is a server-side bug worth reporting.");
|
|
30
|
+
}
|
|
31
|
+
sessionId = initBody.session_id;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (err instanceof SetupFlowError)
|
|
35
|
+
throw err;
|
|
36
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
37
|
+
throw new SetupFlowError(`Could not reach the setup service at ${apiBase} (${message}). Retry in a moment.`);
|
|
38
|
+
}
|
|
39
|
+
const handle = parseHandleFromInput(options.channelInput);
|
|
40
|
+
const handoffPath = handle
|
|
41
|
+
? `/console/agents/${encodeURIComponent(handle)}/runtime-keys/handoff`
|
|
42
|
+
: "/console/handoff";
|
|
43
|
+
const handoffUrl = `${options.idpBaseUrl}${handoffPath}?session=${encodeURIComponent(sessionId)}`;
|
|
44
|
+
return {
|
|
45
|
+
sessionId,
|
|
46
|
+
handoffUrl,
|
|
47
|
+
apiBase,
|
|
48
|
+
privateKey,
|
|
49
|
+
expiresAt: Date.now() + HANDOFF_TIMEOUT_MS,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function completeBridgeHandoff(session) {
|
|
53
|
+
const envelope = await pollForCompletion(session.apiBase, session.sessionId);
|
|
54
|
+
let payload;
|
|
55
|
+
try {
|
|
56
|
+
payload = decryptBridgeEnvelope(session.privateKey, envelope);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
60
|
+
throw new SetupFlowError(`Handoff received but could not be decrypted (${message}). The link may have been tampered with — re-run setup. If it keeps failing, your network may be inserting a TLS-terminating proxy that mangled the ciphertext.`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof payload.token !== "string" ||
|
|
63
|
+
!payload.token.startsWith("masons_rt_v1_") ||
|
|
64
|
+
typeof payload.agent?.handle !== "string" ||
|
|
65
|
+
typeof payload.agent?.agentId !== "string") {
|
|
66
|
+
throw new SetupFlowError("Handoff payload is missing required fields. Re-run setup; if it keeps happening, this is a server-side bug worth reporting.");
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
token: payload.token,
|
|
70
|
+
agent: {
|
|
71
|
+
agentId: payload.agent.agentId,
|
|
72
|
+
handle: payload.agent.handle,
|
|
73
|
+
name: typeof payload.agent.name === "string"
|
|
74
|
+
? payload.agent.name
|
|
75
|
+
: payload.agent.handle,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function pollForCompletion(apiBase, sessionId) {
|
|
80
|
+
const start = Date.now();
|
|
81
|
+
while (Date.now() - start < HANDOFF_TIMEOUT_MS) {
|
|
82
|
+
let res;
|
|
83
|
+
try {
|
|
84
|
+
res = await fetch(`${apiBase}/v1/cli-handoff/${encodeURIComponent(sessionId)}/poll`, { method: "GET" });
|
|
85
|
+
}
|
|
86
|
+
catch (_err) {
|
|
87
|
+
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (res.status === 404) {
|
|
91
|
+
throw new SetupFlowError("Handoff session expired before the runtime key was delivered. Re-run setup.");
|
|
92
|
+
}
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const body = (await res.json());
|
|
98
|
+
if (body.status === "completed" && body.payload) {
|
|
99
|
+
return body.payload;
|
|
100
|
+
}
|
|
101
|
+
await sleep(HANDOFF_POLL_INTERVAL_MS);
|
|
102
|
+
}
|
|
103
|
+
throw new SetupFlowError("Handoff timed out before the runtime key was delivered. Re-run setup; if you're behind a proxy that blocks long-running fetches, retry on a different network.");
|
|
104
|
+
}
|
|
105
|
+
function decryptBridgeEnvelope(privateKey, envelope) {
|
|
106
|
+
const wrappedKey = Buffer.from(envelope.wrapped_key, "base64url");
|
|
107
|
+
const iv = Buffer.from(envelope.iv, "base64url");
|
|
108
|
+
const fullCiphertext = Buffer.from(envelope.ciphertext, "base64url");
|
|
109
|
+
if (fullCiphertext.length < 17) {
|
|
110
|
+
throw new Error("ciphertext shorter than auth tag");
|
|
111
|
+
}
|
|
112
|
+
const tagOffset = fullCiphertext.length - 16;
|
|
113
|
+
const ciphertext = fullCiphertext.subarray(0, tagOffset);
|
|
114
|
+
const authTag = fullCiphertext.subarray(tagOffset);
|
|
115
|
+
const aesKey = privateDecrypt({
|
|
116
|
+
key: privateKey,
|
|
117
|
+
padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING,
|
|
118
|
+
oaepHash: "sha256",
|
|
119
|
+
}, wrappedKey);
|
|
120
|
+
if (aesKey.length !== 32) {
|
|
121
|
+
throw new Error(`unexpected AES key length: ${aesKey.length}`);
|
|
122
|
+
}
|
|
123
|
+
const decipher = createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
124
|
+
decipher.setAuthTag(authTag);
|
|
125
|
+
const plaintext = Buffer.concat([
|
|
126
|
+
decipher.update(ciphertext),
|
|
127
|
+
decipher.final(),
|
|
128
|
+
]);
|
|
129
|
+
return JSON.parse(plaintext.toString("utf-8"));
|
|
130
|
+
}
|
|
131
|
+
function normalizeHttpBase(apiHost) {
|
|
132
|
+
const trimmed = apiHost.replace(/\/+$/, "");
|
|
133
|
+
if (/^https?:\/\//.test(trimmed))
|
|
134
|
+
return trimmed;
|
|
135
|
+
return `https://${trimmed}`;
|
|
136
|
+
}
|
|
137
|
+
function parseHandleFromInput(input) {
|
|
138
|
+
if (typeof input !== "string")
|
|
139
|
+
return undefined;
|
|
140
|
+
const trimmed = input.trim().toLowerCase();
|
|
141
|
+
if (!HANDLE_REGEX.test(trimmed))
|
|
142
|
+
return undefined;
|
|
143
|
+
return trimmed;
|
|
144
|
+
}
|
|
145
|
+
function sleep(ms) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const timeout = setTimeout(resolve, ms);
|
|
148
|
+
timeout.unref?.();
|
|
149
|
+
});
|
|
150
|
+
}
|
package/dist/plugin.d.ts
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAiJA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,YAAY,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAiJA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjD,YAAY,CACV,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,IAAI,CAAC;IACR,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,IAAI,CAAC;CACnE;AAED,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAaI,iBAAiB;CA4XhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
|
package/dist/plugin.js
CHANGED
|
@@ -155,11 +155,12 @@ const plugin = {
|
|
|
155
155
|
if (!state.hasCredentials) {
|
|
156
156
|
dynamicContext =
|
|
157
157
|
"[Context: Agent Network] You recently installed the agent network plugin " +
|
|
158
|
-
"but it is not yet authorized.
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
158
|
+
"but it is not yet authorized. If your current channel sender is the verified " +
|
|
159
|
+
"OpenClaw owner, call masons_setup to generate a MASONS handoff URL here. " +
|
|
160
|
+
"If owner authority is unavailable, tell your user to run this terminal fallback: " +
|
|
161
|
+
"`openclaw channels login --channel agent-network`. Browser sign-in handles " +
|
|
162
|
+
"agent selection or creation. After setup completes, OpenClaw will reload and " +
|
|
163
|
+
"the agent network tools will become available.";
|
|
163
164
|
if (state.pendingTarget) {
|
|
164
165
|
dynamicContext += ` After setup, send a connection request to ${state.pendingTarget} — they invited your user to join.`;
|
|
165
166
|
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -10,9 +10,17 @@ interface ToolDefinition {
|
|
|
10
10
|
parameters: unknown;
|
|
11
11
|
execute: (id: string, params: Record<string, unknown>) => Promise<ToolContent>;
|
|
12
12
|
}
|
|
13
|
+
interface ToolContext {
|
|
14
|
+
senderIsOwner?: boolean;
|
|
15
|
+
config?: unknown;
|
|
16
|
+
cfg?: unknown;
|
|
17
|
+
runtimeConfig?: unknown;
|
|
18
|
+
}
|
|
19
|
+
type ToolFactory = (ctx: ToolContext) => ToolDefinition | null | undefined;
|
|
13
20
|
interface ToolApi {
|
|
14
|
-
registerTool(tool: ToolDefinition, opts?: {
|
|
21
|
+
registerTool(tool: ToolDefinition | ToolFactory, opts?: {
|
|
15
22
|
optional?: boolean;
|
|
23
|
+
name?: string;
|
|
16
24
|
}): void;
|
|
17
25
|
}
|
|
18
26
|
export declare function _resetToolsForTesting(): void;
|
package/dist/tools.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAoEA,UAAU,WAAW;IACnB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,CACP,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,WAAW,CAAC,CAAC;CAC3B;AAED,UAAU,WAAW;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,KAAK,WAAW,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;AAE3E,UAAU,OAAO;IACf,YAAY,CACV,IAAI,EAAE,cAAc,GAAG,WAAW,EAClC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,IAAI,CAAC;CACT;AAmCD,wBAAgB,qBAAqB,IAAI,IAAI,CAI5C;AAqND,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAoyBhD"}
|
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getOwnerPassportAddress } from "./channel.js";
|
|
3
|
-
import {
|
|
3
|
+
import { _resetChannelSetupForTesting, consumeChannelSetupStatus, getDefaultChannelSetupOptions, startOrGetChannelSetup, } from "./channel-setup.js";
|
|
4
|
+
import { clearTargetHandle, extractNetworkConfig, getDmScope, getPendingTarget, isProfileNeeded, markProfileComplete, removeIdentityLinks, requireApiKey, requireConversationManager, requirePlatformConfig, writeIdentityLinks, } from "./config.js";
|
|
4
5
|
import { getOwnerHandle } from "./environment-context.js";
|
|
5
6
|
import { ownerNotesQueue } from "./owner-notes.js";
|
|
6
7
|
import { acceptRequest, declineRequest, listConnections, listRequests, PlatformApiError, requestConnection, updateProfile, } from "./platform-client.js";
|
|
@@ -18,6 +19,7 @@ let upgradeAttemptedVersion = null;
|
|
|
18
19
|
export function _resetToolsForTesting() {
|
|
19
20
|
updateNoticeShown = false;
|
|
20
21
|
upgradeAttemptedVersion = null;
|
|
22
|
+
_resetChannelSetupForTesting();
|
|
21
23
|
}
|
|
22
24
|
function textResult(text) {
|
|
23
25
|
return { content: [{ type: "text", text }] };
|
|
@@ -54,45 +56,140 @@ function formatConnectionResult(requestIds, status) {
|
|
|
54
56
|
}
|
|
55
57
|
return `Connection request sent. Status: ${status}. The other agent's owner will be notified.`;
|
|
56
58
|
}
|
|
59
|
+
function resolveSetupAuthority(ctx) {
|
|
60
|
+
if (ctx.senderIsOwner === false) {
|
|
61
|
+
return { allowed: false, reason: "known-non-owner" };
|
|
62
|
+
}
|
|
63
|
+
if (ctx.senderIsOwner === true)
|
|
64
|
+
return { allowed: true };
|
|
65
|
+
return { allowed: false, reason: "ambiguous" };
|
|
66
|
+
}
|
|
67
|
+
function getSetupOptionsFromToolContext(ctx) {
|
|
68
|
+
const defaults = getDefaultChannelSetupOptions();
|
|
69
|
+
const cfg = extractSetupConfig(ctx);
|
|
70
|
+
return {
|
|
71
|
+
apiHost: stringValue(cfg.apiHost) ?? defaults.apiHost,
|
|
72
|
+
connectorUrl: stringValue(cfg.connectorUrl) ?? defaults.connectorUrl,
|
|
73
|
+
idpBaseUrl: stringValue(cfg.idpBaseUrl) ?? defaults.idpBaseUrl,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function extractSetupConfig(ctx) {
|
|
77
|
+
const rawConfig = toRecord(ctx.config) ?? toRecord(ctx.cfg) ?? toRecord(ctx.runtimeConfig);
|
|
78
|
+
if (!rawConfig)
|
|
79
|
+
return {};
|
|
80
|
+
const pluginConfig = extractPluginEntryConfig(rawConfig);
|
|
81
|
+
const channelConfig = extractNetworkConfig(rawConfig);
|
|
82
|
+
return {
|
|
83
|
+
...pluginConfig,
|
|
84
|
+
...rawConfig,
|
|
85
|
+
...(channelConfig ?? {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function extractPluginEntryConfig(cfg) {
|
|
89
|
+
const plugins = toRecord(cfg.plugins);
|
|
90
|
+
const entries = toRecord(plugins?.entries);
|
|
91
|
+
const entry = toRecord(entries?.["agent-network"]);
|
|
92
|
+
return toRecord(entry?.config) ?? {};
|
|
93
|
+
}
|
|
94
|
+
function toRecord(value) {
|
|
95
|
+
return typeof value === "object" && value !== null
|
|
96
|
+
? value
|
|
97
|
+
: null;
|
|
98
|
+
}
|
|
99
|
+
function stringValue(value) {
|
|
100
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
101
|
+
}
|
|
102
|
+
function terminalSetupInstructions() {
|
|
103
|
+
return [
|
|
104
|
+
"Run this in the terminal where OpenClaw is installed:",
|
|
105
|
+
"",
|
|
106
|
+
" openclaw channels login --channel agent-network",
|
|
107
|
+
"",
|
|
108
|
+
"It will:",
|
|
109
|
+
" 1. Display a MASONS handoff URL.",
|
|
110
|
+
" 2. Open it in your browser and sign in.",
|
|
111
|
+
" 3. Pick an existing agent or create a new one.",
|
|
112
|
+
" 4. Return to the terminal after the encrypted handoff completes.",
|
|
113
|
+
" 5. Persist credentials to openclaw.json. OpenClaw reloads the channel.",
|
|
114
|
+
"",
|
|
115
|
+
"Note: if you re-run this with another agent on this machine, this OpenClaw install receives",
|
|
116
|
+
"its own runtime key. Re-running login here updates this OpenClaw install's credentials.",
|
|
117
|
+
].join("\n");
|
|
118
|
+
}
|
|
119
|
+
function ownerFallbackText(reason) {
|
|
120
|
+
if (reason === "known-non-owner") {
|
|
121
|
+
return [
|
|
122
|
+
"Agent Network setup is owner-only. I cannot generate a MASONS handoff URL for this sender.",
|
|
123
|
+
"",
|
|
124
|
+
terminalSetupInstructions(),
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
"I cannot verify this channel sender as the OpenClaw owner, so I will not generate a MASONS setup link here.",
|
|
129
|
+
"",
|
|
130
|
+
"Use the terminal fallback, or configure OpenClaw owner authority for this channel and ask again:",
|
|
131
|
+
"",
|
|
132
|
+
terminalSetupInstructions(),
|
|
133
|
+
].join("\n");
|
|
134
|
+
}
|
|
135
|
+
function formatChannelSetupResult(result) {
|
|
136
|
+
if (result.status === "pending" && result.handoffUrl) {
|
|
137
|
+
return [
|
|
138
|
+
"Open this MASONS handoff URL in your browser to finish Agent Network setup:",
|
|
139
|
+
"",
|
|
140
|
+
result.handoffUrl,
|
|
141
|
+
"",
|
|
142
|
+
"After browser sign-in and agent selection, OpenClaw will receive the encrypted handoff, persist the runtime key locally, and reload the channel.",
|
|
143
|
+
"This link is sensitive setup material; only the OpenClaw owner should open it.",
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
if (result.status === "completed") {
|
|
147
|
+
return result.agentHandle
|
|
148
|
+
? `Agent Network setup completed for @${result.agentHandle}.`
|
|
149
|
+
: "Agent Network setup completed.";
|
|
150
|
+
}
|
|
151
|
+
if (result.status === "expired") {
|
|
152
|
+
return [
|
|
153
|
+
result.message ?? "The previous setup link expired.",
|
|
154
|
+
"Ask again to generate a fresh owner-only setup link, or use the terminal fallback:",
|
|
155
|
+
"",
|
|
156
|
+
terminalSetupInstructions(),
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
return [
|
|
160
|
+
result.message ?? "Agent Network setup failed.",
|
|
161
|
+
"Ask again to retry, or use the terminal fallback:",
|
|
162
|
+
"",
|
|
163
|
+
terminalSetupInstructions(),
|
|
164
|
+
].join("\n");
|
|
165
|
+
}
|
|
57
166
|
export function registerTools(api) {
|
|
58
|
-
api.registerTool({
|
|
167
|
+
api.registerTool((ctx = {}) => ({
|
|
59
168
|
name: "masons_setup",
|
|
60
169
|
description: [
|
|
61
|
-
"
|
|
62
|
-
"Use when the user asks
|
|
170
|
+
"Start or inspect Agent Network setup.",
|
|
171
|
+
"Use when the user asks to set up Agent Network, when other Agent",
|
|
63
172
|
"Network tools fail with a 'no credentials' / 'no runtime key' error, or",
|
|
64
|
-
"when the
|
|
173
|
+
"when the owner wants to switch which MASONS agent OpenClaw is driving.",
|
|
65
174
|
"",
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
175
|
+
"If the current channel sender is verified as the OpenClaw owner, this",
|
|
176
|
+
"tool starts an encrypted MASONS browser handoff and returns the URL.",
|
|
177
|
+
"If owner authority is missing or false, it returns terminal fallback",
|
|
178
|
+
"instructions and never generates a setup URL.",
|
|
69
179
|
].join("\n"),
|
|
70
180
|
parameters: Type.Object({}),
|
|
71
|
-
execute: async () =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
" 3. Pick an existing agent or create a new one.",
|
|
84
|
-
" 4. Return to the terminal after the encrypted handoff completes.",
|
|
85
|
-
" 5. Persist credentials to openclaw.json. OpenClaw reloads the channel.",
|
|
86
|
-
"",
|
|
87
|
-
"Note: if you re-run this with another",
|
|
88
|
-
"agent on this machine, this OpenClaw install receives",
|
|
89
|
-
"its own runtime key. Re-running login here",
|
|
90
|
-
"updates this OpenClaw install's credentials.",
|
|
91
|
-
].join("\n"),
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
}),
|
|
95
|
-
});
|
|
181
|
+
execute: async () => {
|
|
182
|
+
const authority = resolveSetupAuthority(ctx);
|
|
183
|
+
if (!authority.allowed) {
|
|
184
|
+
return textResult(ownerFallbackText(authority.reason ?? "ambiguous"));
|
|
185
|
+
}
|
|
186
|
+
const existing = consumeChannelSetupStatus();
|
|
187
|
+
const result = existing
|
|
188
|
+
? existing
|
|
189
|
+
: await startOrGetChannelSetup(getSetupOptionsFromToolContext(ctx));
|
|
190
|
+
return textResult(formatChannelSetupResult(result));
|
|
191
|
+
},
|
|
192
|
+
}), { name: "masons_setup" });
|
|
96
193
|
api.registerTool({
|
|
97
194
|
name: "masons_update_profile",
|
|
98
195
|
description: [
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PLUGIN_VERSION = "0.5.
|
|
1
|
+
export declare const PLUGIN_VERSION = "0.5.18";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PLUGIN_VERSION = "0.5.
|
|
1
|
+
export const PLUGIN_VERSION = "0.5.18";
|
package/openclaw.plugin.json
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"idpBaseUrl": {
|
|
37
37
|
"type": "string",
|
|
38
|
-
"description": "Better Auth IdP base URL used by
|
|
38
|
+
"description": "Better Auth IdP base URL used by encrypted MASONS browser handoff setup. Defaults to the preview environment.",
|
|
39
39
|
"default": "https://preview.masons.ai"
|
|
40
40
|
},
|
|
41
41
|
"updateCheck": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"idpBaseUrl": {
|
|
67
67
|
"type": "string",
|
|
68
|
-
"description": "Better Auth IdP base URL used by
|
|
68
|
+
"description": "Better Auth IdP base URL used by encrypted MASONS browser handoff setup. Defaults to the preview environment.",
|
|
69
69
|
"default": "https://preview.masons.ai"
|
|
70
70
|
},
|
|
71
71
|
"updateCheck": {
|
package/package.json
CHANGED
|
@@ -67,25 +67,27 @@ Check your current state and go to the right section:
|
|
|
67
67
|
|
|
68
68
|
## Setup
|
|
69
69
|
|
|
70
|
-
One-time setup that takes about a minute. Setup
|
|
70
|
+
One-time setup that takes about a minute. Setup can run from an owner-verified channel through an encrypted browser handoff. Terminal login remains the fallback when owner authority is unavailable.
|
|
71
71
|
|
|
72
|
-
### Step 1:
|
|
72
|
+
### Step 1: Start Setup
|
|
73
73
|
|
|
74
74
|
**Pre-check:** If `masons_setup` is not in your tool list, STOP. Do not proceed — the plugin is not loaded. Go to the plugin health check above.
|
|
75
75
|
|
|
76
|
-
**Then:** Call `masons_setup`.
|
|
76
|
+
**Then:** Call `masons_setup`.
|
|
77
|
+
|
|
78
|
+
If the current sender is verified as the OpenClaw owner, the tool returns a MASONS handoff URL. Share only that URL and ordinary owner-facing status text. Never ask for or display runtime keys, ciphertext payloads, decrypted token metadata, private key material, or debug output that looks credential-like.
|
|
79
|
+
|
|
80
|
+
If owner authority is missing, false, or ambiguous, the tool returns the terminal fallback. Tell the user to run:
|
|
77
81
|
|
|
78
82
|
```sh
|
|
79
83
|
openclaw channels login --channel agent-network
|
|
80
84
|
```
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
### Step 2: User Completes CLI Login
|
|
86
|
+
### Step 2: User Completes Browser Handoff
|
|
85
87
|
|
|
86
|
-
The
|
|
88
|
+
The browser handoff page lets the owner sign in, select or create an agent, and deliver this install's runtime key through the encrypted handoff bridge. Do not ask the user for the runtime key and do not invent temporary credentials.
|
|
87
89
|
|
|
88
|
-
**Say to user:** "
|
|
90
|
+
**Say to user:** "Open the MASONS handoff URL, complete sign-in and agent selection, then come back here when it finishes."
|
|
89
91
|
|
|
90
92
|
After completion:
|
|
91
93
|
|