Download src/gateway/server/ws-connection/connect-node-pairing-ssh.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 5.76 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/gateway/server/ws-connection/connect-node-pairing-ssh.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/gateway/server/ws-connection/connect-node-pairing-ssh.ts
-
curl -L -o connect-node-pairing-ssh.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/gateway/server/ws-connection/connect-node-pairing-ssh.ts
5.76 kB
| // Gateway WebSocket node pairing can finish a fresh capability-free request over SSH. | |
| import { isDeepStrictEqual } from "node:util"; | |
| import type { ConnectPairingRequiredReason } from "../../../../packages/gateway-protocol/src/connect-error-details.js"; | |
| import { getRuntimeConfigSnapshot } from "../../../config/runtime-snapshot.js"; | |
| import { approveDevicePairing } from "../../../infra/device-pairing-approval.js"; | |
| import { getPairedDevice } from "../../../infra/device-pairing.js"; | |
| import { isScopelessNodePairingRequest } from "../../node-pairing-auto-approve.js"; | |
| import { | |
| planNodePairingSshVerify, | |
| startNodePairingSshVerify, | |
| } from "../../node-pairing-ssh-verify.js"; | |
| import type { | |
| AuthenticatedGatewayConnect, | |
| GatewayConnectPhaseContext, | |
| } from "./message-handler-types.js"; | |
| type PairingRequest = Awaited< | |
| ReturnType<typeof import("../../../infra/device-pairing.js").requestDevicePairing> | |
| >; | |
| export function startGatewayNodePairingSshApproval(params: { | |
| context: GatewayConnectPhaseContext; | |
| state: AuthenticatedGatewayConnect; | |
| pairing: PairingRequest; | |
| existingPairedDevice: Awaited<ReturnType<typeof getPairedDevice>> | null; | |
| devicePublicKey: string; | |
| clientAccessMetadata: { | |
| displayName?: string; | |
| remoteIp?: string; | |
| lastSeenAtMs: number; | |
| lastSeenReason: string; | |
| }; | |
| reason: ConnectPairingRequiredReason; | |
| }): boolean { | |
| const { context, state, pairing, existingPairedDevice, devicePublicKey, clientAccessMetadata } = | |
| params; | |
| const { | |
| connectParams, | |
| configSnapshot, | |
| reportedClientIp, | |
| reportedClientIpSource, | |
| hasBrowserOriginHeader, | |
| runDetachedConnectWork, | |
| } = context; | |
| const { connId, buildRequestContext, logGateway } = context.handler; | |
| const { device, role, scopes, isControlUi, isWebchat } = state; | |
| if (!device || pairing.request.silent === true) { | |
| return false; | |
| } | |
| // Gate on the request actually being approved, not just this | |
| // connect's params: requestDevicePairing can refresh an older | |
| // pending request in place (incomingApprovalCoveredByExisting), so a | |
| // device could seed a scoped pending request, then reconnect | |
| // scopeless from an SSH-verifiable host. SSH auto-approval must stay | |
| // limited to a fresh node request that carries no roles/scopes | |
| // beyond node. | |
| if (!isScopelessNodePairingRequest(pairing.request)) { | |
| return false; | |
| } | |
| const eligibility = { | |
| existingPairedDevice: Boolean(existingPairedDevice), | |
| role, | |
| reason: params.reason, | |
| scopes, | |
| hasBrowserOriginHeader, | |
| isControlUi, | |
| isWebchat, | |
| reportedClientIpSource, | |
| reportedClientIp, | |
| }; | |
| const sshVerifyPlan = planNodePairingSshVerify({ | |
| config: configSnapshot.gateway?.nodes?.pairing?.sshVerify, | |
| eligibility, | |
| }); | |
| const sshVerify = sshVerifyPlan | |
| ? startNodePairingSshVerify({ | |
| plan: sshVerifyPlan, | |
| expectedDeviceId: device.id, | |
| expectedPublicKey: devicePublicKey, | |
| }) | |
| : null; | |
| // A reconnect during an in-flight probe keeps the retry hint | |
| // below but must not attach a second approval tail. | |
| if (sshVerifyPlan && sshVerify && !sshVerify.alreadyInFlight) { | |
| const pendingRequestId = pairing.request.requestId; | |
| runDetachedConnectWork( | |
| async () => { | |
| const outcome = await sshVerify.done; | |
| if (!outcome.ok) { | |
| logGateway.info( | |
| `node pairing ssh-verify did not approve device=${device.id} host=${sshVerifyPlan.host} reason=${outcome.reason}`, | |
| ); | |
| return; | |
| } | |
| const approvedBySsh = await approveDevicePairing(pendingRequestId, { | |
| callerScopes: scopes, | |
| accessMetadata: clientAccessMetadata, | |
| approvedVia: "ssh-verified", | |
| isApprovalCurrent: ({ pending, existing }) => { | |
| const currentConfig = getRuntimeConfigSnapshot(); | |
| if ( | |
| !currentConfig || | |
| pending.deviceId !== device.id || | |
| pending.publicKey !== devicePublicKey || | |
| !isScopelessNodePairingRequest(pending) | |
| ) { | |
| return false; | |
| } | |
| // SSH proves this exact policy, not a later user/key/scope selection. | |
| // The approval owner runs this after lock waits, directly before commit. | |
| return isDeepStrictEqual( | |
| sshVerifyPlan, | |
| planNodePairingSshVerify({ | |
| config: currentConfig.gateway?.nodes?.pairing?.sshVerify, | |
| eligibility: { | |
| ...eligibility, | |
| existingPairedDevice: Boolean(existing), | |
| scopes: pending.scopes ?? [], | |
| }, | |
| }), | |
| ); | |
| }, | |
| }); | |
| if (approvedBySsh?.status !== "approved") { | |
| const reason = | |
| approvedBySsh?.status === "forbidden" | |
| ? approvedBySsh.reason | |
| : "request superseded or already resolved"; | |
| logGateway.info( | |
| `node pairing ssh-verify approval skipped device=${device.id} (${reason})`, | |
| ); | |
| return; | |
| } | |
| logGateway.info( | |
| `security audit: device pairing ssh-verified auto-approve device=${device.id} ip=${reportedClientIp ?? "unknown-ip"} sshUser=${outcome.user} client=${connectParams.client.id} conn=${connId}`, | |
| ); | |
| buildRequestContext().broadcast( | |
| "device.pair.resolved", | |
| { | |
| requestId: pendingRequestId, | |
| deviceId: device.id, | |
| decision: "approved", | |
| ts: Date.now(), | |
| }, | |
| { dropIfSlow: true }, | |
| ); | |
| }, | |
| (error) => { | |
| logGateway.warn(`node pairing ssh-verify failed device=${device.id}: ${String(error)}`); | |
| }, | |
| ); | |
| } | |
| return Boolean(sshVerifyPlan && sshVerify); | |
| } | |