openclaw gateway: persist device keys and smoke pairing flow

This commit is contained in:
Dotta
2026-03-07 17:05:36 -06:00
parent d52f1d4b44
commit 0abb6a1205
6 changed files with 197 additions and 30 deletions

View File

@@ -30,14 +30,22 @@ Open the printed `Dashboard URL` (includes `#token=...`) in your browser.
- Confirm gateway URL is `ws://...` or `wss://...`. - Confirm gateway URL is `ws://...` or `wss://...`.
- Confirm gateway token is non-trivial (not empty / not 1-char placeholder). - Confirm gateway token is non-trivial (not empty / not 1-char placeholder).
- Confirm pairing mode is explicit: - Confirm pairing mode is explicit:
- smoke/dev default: set `adapterConfig.disableDeviceAuth=true` to avoid interactive pairing prompts on each run - recommended default: `adapterConfig.disableDeviceAuth` is false/absent and `adapterConfig.devicePrivateKeyPem` is present
- if keeping device auth enabled: set a stable `adapterConfig.devicePrivateKeyPem` so pairing is approved once and reused - fallback only: `adapterConfig.disableDeviceAuth=true` when pairing cannot be supported in that environment
- If you can run API checks with board auth: - If you can run API checks with board auth:
```bash ```bash
AGENT_ID="<newly-created-agent-id>" AGENT_ID="<newly-created-agent-id>"
curl -sS -H "Cookie: $PAPERCLIP_COOKIE" "http://127.0.0.1:3100/api/agents/$AGENT_ID" | jq '{adapterType,adapterConfig:{url:.adapterConfig.url,tokenLen:(.adapterConfig.headers["x-openclaw-token"] // .adapterConfig.headers["x-openclaw-auth"] // "" | length),disableDeviceAuth:(.adapterConfig.disableDeviceAuth // false),hasDeviceKey:(.adapterConfig.devicePrivateKeyPem // "" | length > 0)}}' curl -sS -H "Cookie: $PAPERCLIP_COOKIE" "http://127.0.0.1:3100/api/agents/$AGENT_ID" | jq '{adapterType,adapterConfig:{url:.adapterConfig.url,tokenLen:(.adapterConfig.headers["x-openclaw-token"] // .adapterConfig.headers["x-openclaw-auth"] // "" | length),disableDeviceAuth:(.adapterConfig.disableDeviceAuth // false),hasDeviceKey:(.adapterConfig.devicePrivateKeyPem // "" | length > 0)}}'
``` ```
- Expected: `adapterType=openclaw_gateway`, `tokenLen >= 16`, and (`disableDeviceAuth=true` OR `hasDeviceKey=true`). - Expected: `adapterType=openclaw_gateway`, `tokenLen >= 16`, `hasDeviceKey=true`, and `disableDeviceAuth=false`.
Pairing handshake note:
- The first gateway run may return `pairing required` once for a new device key.
- Approve it in OpenClaw, then retry the task.
- For local docker smoke, you can approve from host:
```bash
docker exec openclaw-docker-openclaw-gateway-1 sh -lc 'openclaw devices approve --latest --json --url "ws://127.0.0.1:18789" --token "$(node -p \"require(process.env.HOME+\\\"/.openclaw/openclaw.json\\\").gateway.auth.token\")"'
```
7. Case A (manual issue test). 7. Case A (manual issue test).
- Create an issue assigned to the OpenClaw agent. - Create an issue assigned to the OpenClaw agent.
@@ -63,7 +71,7 @@ docker compose -f /tmp/openclaw-docker/docker-compose.yml -f /tmp/openclaw-docke
11. Expected pass criteria. 11. Expected pass criteria.
- Preflight: `openclaw_gateway` + non-placeholder token (`tokenLen >= 16`). - Preflight: `openclaw_gateway` + non-placeholder token (`tokenLen >= 16`).
- Pairing mode: either `disableDeviceAuth=true` (smoke/dev) or stable `devicePrivateKeyPem` configured. - Pairing mode: stable `devicePrivateKeyPem` configured with device auth enabled (default path).
- Case A: `done` + marker comment. - Case A: `done` + marker comment.
- Case B: `done` + marker comment + main-chat message visible. - Case B: `done` + marker comment + main-chat message visible.
- Case C: original task done and new issue created from `/new` session. - Case C: original task done and new issue created from `/new` session.

View File

@@ -250,7 +250,6 @@ POST /api/companies/$CLA_COMPANY_ID/invites
"headers": { "x-openclaw-token": "<gateway-token>" }, "headers": { "x-openclaw-token": "<gateway-token>" },
"role": "operator", "role": "operator",
"scopes": ["operator.admin"], "scopes": ["operator.admin"],
"disableDeviceAuth": true,
"sessionKeyStrategy": "fixed", "sessionKeyStrategy": "fixed",
"sessionKey": "paperclip", "sessionKey": "paperclip",
"waitTimeoutMs": 120000 "waitTimeoutMs": 120000
@@ -265,13 +264,17 @@ POST /api/companies/$CLA_COMPANY_ID/invites
- `adapterConfig.headers.x-openclaw-token` exists and is not placeholder/too-short (`len >= 16`) - `adapterConfig.headers.x-openclaw-token` exists and is not placeholder/too-short (`len >= 16`)
- token hash matches the OpenClaw `gateway.auth.token` used for join - token hash matches the OpenClaw `gateway.auth.token` used for join
- pairing mode is explicit: - pairing mode is explicit:
- smoke/dev: `adapterConfig.disableDeviceAuth == true` (no interactive pairing gate) - default path: `adapterConfig.disableDeviceAuth` is false/absent and stable `adapterConfig.devicePrivateKeyPem` is set so approvals persist across runs
- otherwise: stable `adapterConfig.devicePrivateKeyPem` is set so approvals persist across runs - fallback path: `disableDeviceAuth=true` only for environments that cannot support pairing
5. Claim API key with `claimSecret`. 5. Trigger one connectivity run. If it returns `pairing required`, approve the pending device request in OpenClaw and retry once.
6. Save claimed token to OpenClaw expected file path (`~/.openclaw/workspace/paperclip-claimed-api-key.json`) and ensure `PAPERCLIP_API_KEY` + `PAPERCLIP_API_URL` are available for OpenClaw skill execution context. - Local docker automation path:
- `openclaw devices approve --latest --json --url ws://127.0.0.1:18789 --token <gateway-token>`
- After approval, retries should succeed using the persisted `devicePrivateKeyPem`.
6. Claim API key with `claimSecret`.
7. Save claimed token to OpenClaw expected file path (`~/.openclaw/workspace/paperclip-claimed-api-key.json`) and ensure `PAPERCLIP_API_KEY` + `PAPERCLIP_API_URL` are available for OpenClaw skill execution context.
- Write compatibility JSON keys (`token` and `apiKey`) to avoid runtime parser mismatch. - Write compatibility JSON keys (`token` and `apiKey`) to avoid runtime parser mismatch.
7. Ensure Paperclip skill is installed for OpenClaw runtime. 8. Ensure Paperclip skill is installed for OpenClaw runtime.
8. Send one bootstrap prompt to OpenClaw containing all setup instructions needed for this run (auth file usage, heartbeat procedure, required tools). If needed, send one follow-up nudge only. 9. Send one bootstrap prompt to OpenClaw containing all setup instructions needed for this run (auth file usage, heartbeat procedure, required tools). If needed, send one follow-up nudge only.
## 6) E2E Validation Cases ## 6) E2E Validation Cases
@@ -322,7 +325,7 @@ Responsibilities:
- Old OpenClaw agent cleanup. - Old OpenClaw agent cleanup.
- Invite/join/approve/claim orchestration. - Invite/join/approve/claim orchestration.
- Gateway agent config/token preflight validation before connectivity or case execution. - Gateway agent config/token preflight validation before connectivity or case execution.
- Pairing-mode preflight (`disableDeviceAuth=true` for smoke/dev or stable `devicePrivateKeyPem`). - Pairing-mode preflight (`disableDeviceAuth=false` + stable `devicePrivateKeyPem` by default).
- E2E case execution + assertions. - E2E case execution + assertions.
- Final summary with run IDs, issue IDs, agent ID. - Final summary with run IDs, issue IDs, agent ID.

View File

@@ -53,6 +53,7 @@ AUTO_INSTALL_SKILL="${AUTO_INSTALL_SKILL:-1}"
OPENCLAW_DIAG_DIR="${OPENCLAW_DIAG_DIR:-/tmp/openclaw-gateway-e2e-diag-$(date +%Y%m%d-%H%M%S)}" OPENCLAW_DIAG_DIR="${OPENCLAW_DIAG_DIR:-/tmp/openclaw-gateway-e2e-diag-$(date +%Y%m%d-%H%M%S)}"
OPENCLAW_ADAPTER_TIMEOUT_SEC="${OPENCLAW_ADAPTER_TIMEOUT_SEC:-120}" OPENCLAW_ADAPTER_TIMEOUT_SEC="${OPENCLAW_ADAPTER_TIMEOUT_SEC:-120}"
OPENCLAW_ADAPTER_WAIT_TIMEOUT_MS="${OPENCLAW_ADAPTER_WAIT_TIMEOUT_MS:-120000}" OPENCLAW_ADAPTER_WAIT_TIMEOUT_MS="${OPENCLAW_ADAPTER_WAIT_TIMEOUT_MS:-120000}"
PAIRING_AUTO_APPROVE="${PAIRING_AUTO_APPROVE:-1}"
PAYLOAD_TEMPLATE_MESSAGE_APPEND="${PAYLOAD_TEMPLATE_MESSAGE_APPEND:-}" PAYLOAD_TEMPLATE_MESSAGE_APPEND="${PAYLOAD_TEMPLATE_MESSAGE_APPEND:-}"
AUTH_HEADERS=() AUTH_HEADERS=()
@@ -418,7 +419,6 @@ create_and_approve_gateway_join() {
headers: { "x-openclaw-token": $token }, headers: { "x-openclaw-token": $token },
role: "operator", role: "operator",
scopes: ["operator.admin"], scopes: ["operator.admin"],
disableDeviceAuth: true,
sessionKeyStrategy: "fixed", sessionKeyStrategy: "fixed",
sessionKey: "paperclip", sessionKey: "paperclip",
timeoutSec: $timeoutSec, timeoutSec: $timeoutSec,
@@ -530,10 +530,12 @@ validate_joined_gateway_agent() {
api_request "GET" "/agents/${AGENT_ID}" api_request "GET" "/agents/${AGENT_ID}"
assert_status "200" assert_status "200"
local adapter_type gateway_url configured_token local adapter_type gateway_url configured_token disable_device_auth device_key_len
adapter_type="$(jq -r '.adapterType // empty' <<<"$RESPONSE_BODY")" adapter_type="$(jq -r '.adapterType // empty' <<<"$RESPONSE_BODY")"
gateway_url="$(jq -r '.adapterConfig.url // empty' <<<"$RESPONSE_BODY")" gateway_url="$(jq -r '.adapterConfig.url // empty' <<<"$RESPONSE_BODY")"
configured_token="$(jq -r '.adapterConfig.headers["x-openclaw-token"] // .adapterConfig.headers["x-openclaw-auth"] // empty' <<<"$RESPONSE_BODY")" configured_token="$(jq -r '.adapterConfig.headers["x-openclaw-token"] // .adapterConfig.headers["x-openclaw-auth"] // empty' <<<"$RESPONSE_BODY")"
disable_device_auth="$(jq -r 'if .adapterConfig.disableDeviceAuth == true then "true" else "false" end' <<<"$RESPONSE_BODY")"
device_key_len="$(jq -r '(.adapterConfig.devicePrivateKeyPem // "" | length)' <<<"$RESPONSE_BODY")"
[[ "$adapter_type" == "openclaw_gateway" ]] || fail "joined agent adapterType is '${adapter_type}', expected 'openclaw_gateway'" [[ "$adapter_type" == "openclaw_gateway" ]] || fail "joined agent adapterType is '${adapter_type}', expected 'openclaw_gateway'"
[[ "$gateway_url" =~ ^wss?:// ]] || fail "joined agent gateway url is invalid: '${gateway_url}'" [[ "$gateway_url" =~ ^wss?:// ]] || fail "joined agent gateway url is invalid: '${gateway_url}'"
@@ -549,9 +551,46 @@ validate_joined_gateway_agent() {
fail "joined agent gateway token hash mismatch (expected ${expected_hash}, got ${configured_hash})" fail "joined agent gateway token hash mismatch (expected ${expected_hash}, got ${configured_hash})"
fi fi
[[ "$disable_device_auth" == "false" ]] || fail "joined agent has disableDeviceAuth=true; smoke requires device auth enabled with persistent key"
if (( device_key_len < 32 )); then
fail "joined agent missing persistent devicePrivateKeyPem (length=${device_key_len})"
fi
log "validated joined gateway agent config (token sha256 prefix ${configured_hash})" log "validated joined gateway agent config (token sha256 prefix ${configured_hash})"
} }
run_log_contains_pairing_required() {
local run_id="$1"
api_request "GET" "/heartbeat-runs/${run_id}/log?limitBytes=262144"
if [[ "$RESPONSE_CODE" != "200" ]]; then
return 1
fi
local content
content="$(jq -r '.content // ""' <<<"$RESPONSE_BODY")"
grep -qi "pairing required" <<<"$content"
}
approve_latest_pairing_request() {
local gateway_token="$1"
local container
container="$(detect_openclaw_container || true)"
[[ -n "$container" ]] || return 1
log "approving latest gateway pairing request in ${container}"
local output
if output="$(docker exec \
-e OPENCLAW_GATEWAY_URL="$OPENCLAW_GATEWAY_URL" \
-e OPENCLAW_GATEWAY_TOKEN="$gateway_token" \
"$container" \
sh -lc 'openclaw devices approve --latest --json --url "$OPENCLAW_GATEWAY_URL" --token "$OPENCLAW_GATEWAY_TOKEN"' 2>&1)"; then
log "pairing approval response: $(printf "%s" "$output" | tr '\n' ' ' | cut -c1-400)"
return 0
fi
warn "pairing auto-approve failed: $(printf "%s" "$output" | tr '\n' ' ' | cut -c1-400)"
return 1
}
trigger_wakeup() { trigger_wakeup() {
local reason="$1" local reason="$1"
local issue_id="${2:-}" local issue_id="${2:-}"
@@ -871,13 +910,30 @@ main() {
log "joined/approved agent ${AGENT_ID} invite=${INVITE_ID} joinRequest=${JOIN_REQUEST_ID}" log "joined/approved agent ${AGENT_ID} invite=${INVITE_ID} joinRequest=${JOIN_REQUEST_ID}"
validate_joined_gateway_agent "$gateway_token" validate_joined_gateway_agent "$gateway_token"
trigger_wakeup "openclaw_gateway_smoke_connectivity" local connect_status="unknown"
if [[ -n "$RUN_ID" ]]; then local connect_attempt
local connect_status for connect_attempt in 1 2; do
trigger_wakeup "openclaw_gateway_smoke_connectivity_attempt_${connect_attempt}"
if [[ -z "$RUN_ID" ]]; then
connect_status="unknown"
break
fi
connect_status="$(wait_for_run_terminal "$RUN_ID" "$RUN_TIMEOUT_SEC")" connect_status="$(wait_for_run_terminal "$RUN_ID" "$RUN_TIMEOUT_SEC")"
[[ "$connect_status" == "succeeded" ]] || fail "connectivity wake run failed: ${connect_status}" if [[ "$connect_status" == "succeeded" ]]; then
log "connectivity wake run ${RUN_ID} succeeded" log "connectivity wake run ${RUN_ID} succeeded (attempt=${connect_attempt})"
fi break
fi
if [[ "$PAIRING_AUTO_APPROVE" == "1" && "$connect_attempt" -eq 1 ]] && run_log_contains_pairing_required "$RUN_ID"; then
log "connectivity run hit pairing gate; attempting one-time pairing approval"
approve_latest_pairing_request "$gateway_token" || fail "pairing approval failed after pairing-required run ${RUN_ID}"
sleep 2
continue
fi
fail "connectivity wake run failed: ${connect_status} (attempt=${connect_attempt}, runId=${RUN_ID})"
done
[[ "$connect_status" == "succeeded" ]] || fail "connectivity wake run did not succeed after retries"
run_case_a run_case_a
run_case_b run_case_b

View File

@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { buildJoinDefaultsPayloadForAccept } from "../routes/access.js"; import {
buildJoinDefaultsPayloadForAccept,
normalizeAgentDefaultsForJoin,
} from "../routes/access.js";
describe("buildJoinDefaultsPayloadForAccept", () => { describe("buildJoinDefaultsPayloadForAccept", () => {
it("maps OpenClaw compatibility fields into agent defaults", () => { it("maps OpenClaw compatibility fields into agent defaults", () => {
@@ -245,4 +248,47 @@ describe("buildJoinDefaultsPayloadForAccept", () => {
}, },
}); });
}); });
it("generates persistent device key for openclaw_gateway when device auth is enabled", () => {
const normalized = normalizeAgentDefaultsForJoin({
adapterType: "openclaw_gateway",
defaultsPayload: {
url: "ws://127.0.0.1:18789",
headers: {
"x-openclaw-token": "gateway-token-1234567890",
},
disableDeviceAuth: false,
},
deploymentMode: "authenticated",
deploymentExposure: "private",
bindHost: "127.0.0.1",
allowedHostnames: [],
});
expect(normalized.fatalErrors).toEqual([]);
expect(normalized.normalized?.disableDeviceAuth).toBe(false);
expect(typeof normalized.normalized?.devicePrivateKeyPem).toBe("string");
expect((normalized.normalized?.devicePrivateKeyPem as string).length).toBeGreaterThan(64);
});
it("does not generate device key when openclaw_gateway has disableDeviceAuth=true", () => {
const normalized = normalizeAgentDefaultsForJoin({
adapterType: "openclaw_gateway",
defaultsPayload: {
url: "ws://127.0.0.1:18789",
headers: {
"x-openclaw-token": "gateway-token-1234567890",
},
disableDeviceAuth: true,
},
deploymentMode: "authenticated",
deploymentExposure: "private",
bindHost: "127.0.0.1",
allowedHostnames: [],
});
expect(normalized.fatalErrors).toEqual([]);
expect(normalized.normalized?.disableDeviceAuth).toBe(true);
expect(normalized.normalized?.devicePrivateKeyPem).toBeUndefined();
});
}); });

View File

@@ -1,4 +1,9 @@
import { createHash, randomBytes, timingSafeEqual } from "node:crypto"; import {
createHash,
generateKeyPairSync,
randomBytes,
timingSafeEqual
} from "node:crypto";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -330,6 +335,13 @@ function parseBooleanLike(value: unknown): boolean | null {
return null; return null;
} }
function generateEd25519PrivateKeyPem(): string {
const generated = generateKeyPairSync("ed25519");
return generated.privateKey
.export({ type: "pkcs8", format: "pem" })
.toString();
}
export function buildJoinDefaultsPayloadForAccept(input: { export function buildJoinDefaultsPayloadForAccept(input: {
adapterType: string | null; adapterType: string | null;
defaultsPayload: unknown; defaultsPayload: unknown;
@@ -611,10 +623,16 @@ function summarizeOpenClawGatewayDefaultsForLog(defaultsPayload: unknown) {
sessionKeyStrategy: defaults sessionKeyStrategy: defaults
? nonEmptyTrimmedString(defaults.sessionKeyStrategy) ? nonEmptyTrimmedString(defaults.sessionKeyStrategy)
: null, : null,
disableDeviceAuth: defaults
? parseBooleanLike(defaults.disableDeviceAuth)
: null,
waitTimeoutMs: waitTimeoutMs:
defaults && typeof defaults.waitTimeoutMs === "number" defaults && typeof defaults.waitTimeoutMs === "number"
? defaults.waitTimeoutMs ? defaults.waitTimeoutMs
: null, : null,
devicePrivateKeyPem: defaults
? summarizeSecretForLog(defaults.devicePrivateKeyPem)
: null,
gatewayToken: summarizeSecretForLog(gatewayTokenValue) gatewayToken: summarizeSecretForLog(gatewayTokenValue)
}; };
} }
@@ -692,7 +710,7 @@ function buildJoinConnectivityDiagnostics(input: {
return diagnostics; return diagnostics;
} }
function normalizeAgentDefaultsForJoin(input: { export function normalizeAgentDefaultsForJoin(input: {
adapterType: string | null; adapterType: string | null;
defaultsPayload: unknown; defaultsPayload: unknown;
deploymentMode: DeploymentMode; deploymentMode: DeploymentMode;
@@ -828,10 +846,47 @@ function normalizeAgentDefaultsForJoin(input: {
} }
const parsedDisableDeviceAuth = parseBooleanLike(defaults.disableDeviceAuth); const parsedDisableDeviceAuth = parseBooleanLike(defaults.disableDeviceAuth);
const disableDeviceAuth = parsedDisableDeviceAuth === true;
if (parsedDisableDeviceAuth !== null) { if (parsedDisableDeviceAuth !== null) {
normalized.disableDeviceAuth = parsedDisableDeviceAuth; normalized.disableDeviceAuth = parsedDisableDeviceAuth;
} }
const configuredDevicePrivateKeyPem = nonEmptyTrimmedString(
defaults.devicePrivateKeyPem
);
if (configuredDevicePrivateKeyPem) {
normalized.devicePrivateKeyPem = configuredDevicePrivateKeyPem;
diagnostics.push({
code: "openclaw_gateway_device_key_configured",
level: "info",
message:
"Gateway device key configured. Pairing approvals should persist for this agent."
});
} else if (!disableDeviceAuth) {
try {
normalized.devicePrivateKeyPem = generateEd25519PrivateKeyPem();
diagnostics.push({
code: "openclaw_gateway_device_key_generated",
level: "info",
message:
"Generated persistent gateway device key for this join. Pairing approvals should persist for this agent."
});
} catch (err) {
diagnostics.push({
code: "openclaw_gateway_device_key_generate_failed",
level: "warn",
message: `Failed to generate gateway device key: ${
err instanceof Error ? err.message : String(err)
}`,
hint:
"Set agentDefaultsPayload.devicePrivateKeyPem explicitly or set disableDeviceAuth=true."
});
fatalErrors.push(
"Failed to generate gateway device key. Set devicePrivateKeyPem or disableDeviceAuth=true."
);
}
}
const waitTimeoutMs = const waitTimeoutMs =
typeof defaults.waitTimeoutMs === "number" && typeof defaults.waitTimeoutMs === "number" &&
Number.isFinite(defaults.waitTimeoutMs) Number.isFinite(defaults.waitTimeoutMs)
@@ -1293,7 +1348,7 @@ function buildInviteOnboardingManifest(
adapterType: "Use 'openclaw_gateway' for OpenClaw Gateway agents", adapterType: "Use 'openclaw_gateway' for OpenClaw Gateway agents",
capabilities: "Optional capability summary", capabilities: "Optional capability summary",
agentDefaultsPayload: agentDefaultsPayload:
"Adapter config for OpenClaw gateway. MUST include url (ws:// or wss://) and headers.x-openclaw-token (or legacy x-openclaw-auth). Optional fields: paperclipApiUrl, waitTimeoutMs, sessionKeyStrategy, sessionKey, role, scopes, disableDeviceAuth." "Adapter config for OpenClaw gateway. MUST include url (ws:// or wss://) and headers.x-openclaw-token (or legacy x-openclaw-auth). Optional fields: paperclipApiUrl, waitTimeoutMs, sessionKeyStrategy, sessionKey, role, scopes, disableDeviceAuth, devicePrivateKeyPem."
}, },
registrationEndpoint: { registrationEndpoint: {
method: "POST", method: "POST",
@@ -1430,7 +1485,6 @@ export function buildInviteOnboardingTextDocument(
waitTimeoutMs: 120000, waitTimeoutMs: 120000,
sessionKeyStrategy: "fixed", sessionKeyStrategy: "fixed",
sessionKey: "paperclip", sessionKey: "paperclip",
disableDeviceAuth: true,
role: "operator", role: "operator",
scopes: ["operator.admin"] scopes: ["operator.admin"]
} }
@@ -1447,8 +1501,9 @@ export function buildInviteOnboardingTextDocument(
Legacy x-openclaw-auth is also accepted, but x-openclaw-token is preferred. Legacy x-openclaw-auth is also accepted, but x-openclaw-token is preferred.
Use adapterType "openclaw_gateway" and a ws:// or wss:// gateway URL. Use adapterType "openclaw_gateway" and a ws:// or wss:// gateway URL.
Pairing mode requirement: Pairing mode requirement:
- For smoke/dev, set "disableDeviceAuth": true to avoid interactive pairing blocks. - Keep device auth enabled (recommended). If devicePrivateKeyPem is omitted, Paperclip generates and persists one during join so pairing approvals are stable.
- If device auth remains enabled, set a stable "devicePrivateKeyPem"; otherwise each run may generate a new device identity and trigger pairing again. - You may set disableDeviceAuth=true only for special environments that cannot support pairing.
- First run may return "pairing required" once; approve the pending pairing request in OpenClaw, then retry.
Do NOT use /v1/responses or /hooks/* in this gateway join flow. Do NOT use /v1/responses or /hooks/* in this gateway join flow.
Body (JSON): Body (JSON):
@@ -1464,7 +1519,6 @@ export function buildInviteOnboardingTextDocument(
"waitTimeoutMs": 120000, "waitTimeoutMs": 120000,
"sessionKeyStrategy": "fixed", "sessionKeyStrategy": "fixed",
"sessionKey": "paperclip", "sessionKey": "paperclip",
"disableDeviceAuth": true,
"role": "operator", "role": "operator",
"scopes": ["operator.admin"] "scopes": ["operator.admin"]
} }

View File

@@ -486,8 +486,8 @@ When you submit the join request, use:
- \`agentDefaultsPayload.url\` as your \`ws://\` or \`wss://\` gateway URL - \`agentDefaultsPayload.url\` as your \`ws://\` or \`wss://\` gateway URL
- \`agentDefaultsPayload.headers["x-openclaw-token"]\` with your gateway token - \`agentDefaultsPayload.headers["x-openclaw-token"]\` with your gateway token
- (legacy accepted) \`agentDefaultsPayload.headers["x-openclaw-auth"]\` - (legacy accepted) \`agentDefaultsPayload.headers["x-openclaw-auth"]\`
- For stock smoke/dev onboarding: set \`agentDefaultsPayload.disableDeviceAuth = true\` to avoid repeated pairing prompts. - Keep device auth enabled (recommended). If \`devicePrivateKeyPem\` is omitted, Paperclip will generate and persist one during join so pairing approvals remain stable.
- If keeping device auth enabled, provide a stable \`agentDefaultsPayload.devicePrivateKeyPem\`; otherwise a new ephemeral device ID may require pairing every run. - Only use \`disableDeviceAuth=true\` for special environments where pairing cannot be completed.
Do NOT use \`/v1/responses\` or \`/hooks/*\` in this join flow. Do NOT use \`/v1/responses\` or \`/hooks/*\` in this join flow.