@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.
@@ -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;
@@ -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;AAyWD,wBAAsB,KAAK,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqDhE"}
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 { publicKey, privateKey } = generateKeyPairSync("rsa", {
12
- modulusLength: 2048,
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
- const envelope = await pollForCompletion(apiBase, session_id);
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"}
@@ -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
@@ -5,6 +5,7 @@ interface OpenClawPluginApi {
5
5
  }): void;
6
6
  registerTool(tool: unknown, opts?: {
7
7
  optional?: boolean;
8
+ name?: string;
8
9
  }): void;
9
10
  on(event: string, handler: (...args: unknown[]) => unknown): void;
10
11
  }
@@ -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,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,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;CA0XhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
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. Tell your user to run this in their terminal: " +
159
- "`openclaw channels login --channel agent-network`. " +
160
- "The command prints a MASONS handoff URL; the browser sign-in handles " +
161
- "agent selection or creation. After it completes, OpenClaw will " +
162
- "reload and the agent network tools will become available.";
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;
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AA4DA,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,OAAO;IACf,YAAY,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CACzE;AAmCD,wBAAgB,qBAAqB,IAAI,IAAI,CAG5C;AAoFD,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAizBhD"}
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 { clearTargetHandle, getDmScope, getPendingTarget, isProfileNeeded, markProfileComplete, removeIdentityLinks, requireApiKey, requireConversationManager, requirePlatformConfig, writeIdentityLinks, } from "./config.js";
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
- "Returns instructions for connecting OpenClaw to the agent network.",
62
- "Use when the user asks how to set up Agent Network, when other Agent",
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 user wants to switch which agent OpenClaw is driving.",
173
+ "when the owner wants to switch which MASONS agent OpenClaw is driving.",
65
174
  "",
66
- "This tool does NOT perform setup itself it surfaces the terminal",
67
- "command. The user must run it from their shell because login requires",
68
- "a browser handoff.",
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
- content: [
73
- {
74
- type: "text",
75
- text: [
76
- "Run this in the terminal where OpenClaw is installed:",
77
- "",
78
- " openclaw channels login --channel agent-network",
79
- "",
80
- "It will:",
81
- " 1. Display a MASONS handoff URL.",
82
- " 2. Open it in your browser and sign in.",
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.16";
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.16";
1
+ export const PLUGIN_VERSION = "0.5.18";
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "idpBaseUrl": {
37
37
  "type": "string",
38
- "description": "Better Auth IdP base URL used by `openclaw channels login --channel agent-network` for the encrypted handoff. Defaults to the preview environment.",
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 `openclaw channels login --channel agent-network` for the encrypted handoff. Defaults to the preview environment.",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masons/agent-network",
3
- "version": "0.5.16",
3
+ "version": "0.5.18",
4
4
  "description": "MASONS plugin for OpenClaw — connect your agent to the agent network",
5
5
  "license": "MIT",
6
6
  "author": "MASONS.ai <hello@masons.ai> (https://masons.ai)",
@@ -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 is CLI-owned because it needs a browser handoff and terminal output.
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: Surface the Login Command
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`. It returns the terminal command the user must run:
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
- **Say to user:** "I'll connect this OpenClaw install to the agent network. Please run the command in your terminal, follow the browser sign-in, then come back here when it finishes."
83
-
84
- ### Step 2: User Completes CLI Login
86
+ ### Step 2: User Completes Browser Handoff
85
87
 
86
- The CLI login flow opens a MASONS handoff page, lets the user select or create an agent, and persists this install's runtime key after the encrypted handoff completes. Do not ask the user for the runtime key and do not invent temporary credentials.
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:** "Once the terminal says the login finished, tell me and I'll continue."
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