openclaw gateway: persist device keys and smoke pairing flow
This commit is contained in:
@@ -30,14 +30,22 @@ Open the printed `Dashboard URL` (includes `#token=...`) in your browser.
|
||||
- Confirm gateway URL is `ws://...` or `wss://...`.
|
||||
- Confirm gateway token is non-trivial (not empty / not 1-char placeholder).
|
||||
- Confirm pairing mode is explicit:
|
||||
- smoke/dev default: set `adapterConfig.disableDeviceAuth=true` to avoid interactive pairing prompts on each run
|
||||
- if keeping device auth enabled: set a stable `adapterConfig.devicePrivateKeyPem` so pairing is approved once and reused
|
||||
- recommended default: `adapterConfig.disableDeviceAuth` is false/absent and `adapterConfig.devicePrivateKeyPem` is present
|
||||
- fallback only: `adapterConfig.disableDeviceAuth=true` when pairing cannot be supported in that environment
|
||||
- If you can run API checks with board auth:
|
||||
```bash
|
||||
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)}}'
|
||||
```
|
||||
- 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).
|
||||
- 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.
|
||||
- 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 B: `done` + marker comment + main-chat message visible.
|
||||
- Case C: original task done and new issue created from `/new` session.
|
||||
|
||||
@@ -250,7 +250,6 @@ POST /api/companies/$CLA_COMPANY_ID/invites
|
||||
"headers": { "x-openclaw-token": "<gateway-token>" },
|
||||
"role": "operator",
|
||||
"scopes": ["operator.admin"],
|
||||
"disableDeviceAuth": true,
|
||||
"sessionKeyStrategy": "fixed",
|
||||
"sessionKey": "paperclip",
|
||||
"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`)
|
||||
- token hash matches the OpenClaw `gateway.auth.token` used for join
|
||||
- pairing mode is explicit:
|
||||
- smoke/dev: `adapterConfig.disableDeviceAuth == true` (no interactive pairing gate)
|
||||
- otherwise: stable `adapterConfig.devicePrivateKeyPem` is set so approvals persist across runs
|
||||
5. Claim API key with `claimSecret`.
|
||||
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.
|
||||
- default path: `adapterConfig.disableDeviceAuth` is false/absent and stable `adapterConfig.devicePrivateKeyPem` is set so approvals persist across runs
|
||||
- fallback path: `disableDeviceAuth=true` only for environments that cannot support pairing
|
||||
5. Trigger one connectivity run. If it returns `pairing required`, approve the pending device request in OpenClaw and retry once.
|
||||
- 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.
|
||||
7. 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.
|
||||
8. Ensure Paperclip skill is installed for OpenClaw runtime.
|
||||
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
|
||||
|
||||
@@ -322,7 +325,7 @@ Responsibilities:
|
||||
- Old OpenClaw agent cleanup.
|
||||
- Invite/join/approve/claim orchestration.
|
||||
- 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.
|
||||
- Final summary with run IDs, issue IDs, agent ID.
|
||||
|
||||
|
||||
@@ -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_ADAPTER_TIMEOUT_SEC="${OPENCLAW_ADAPTER_TIMEOUT_SEC:-120}"
|
||||
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:-}"
|
||||
|
||||
AUTH_HEADERS=()
|
||||
@@ -418,7 +419,6 @@ create_and_approve_gateway_join() {
|
||||
headers: { "x-openclaw-token": $token },
|
||||
role: "operator",
|
||||
scopes: ["operator.admin"],
|
||||
disableDeviceAuth: true,
|
||||
sessionKeyStrategy: "fixed",
|
||||
sessionKey: "paperclip",
|
||||
timeoutSec: $timeoutSec,
|
||||
@@ -530,10 +530,12 @@ validate_joined_gateway_agent() {
|
||||
api_request "GET" "/agents/${AGENT_ID}"
|
||||
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")"
|
||||
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")"
|
||||
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'"
|
||||
[[ "$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})"
|
||||
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})"
|
||||
}
|
||||
|
||||
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() {
|
||||
local reason="$1"
|
||||
local issue_id="${2:-}"
|
||||
@@ -871,13 +910,30 @@ main() {
|
||||
log "joined/approved agent ${AGENT_ID} invite=${INVITE_ID} joinRequest=${JOIN_REQUEST_ID}"
|
||||
validate_joined_gateway_agent "$gateway_token"
|
||||
|
||||
trigger_wakeup "openclaw_gateway_smoke_connectivity"
|
||||
if [[ -n "$RUN_ID" ]]; then
|
||||
local connect_status
|
||||
local connect_status="unknown"
|
||||
local connect_attempt
|
||||
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" == "succeeded" ]] || fail "connectivity wake run failed: ${connect_status}"
|
||||
log "connectivity wake run ${RUN_ID} succeeded"
|
||||
fi
|
||||
if [[ "$connect_status" == "succeeded" ]]; then
|
||||
log "connectivity wake run ${RUN_ID} succeeded (attempt=${connect_attempt})"
|
||||
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_b
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildJoinDefaultsPayloadForAccept } from "../routes/access.js";
|
||||
import {
|
||||
buildJoinDefaultsPayloadForAccept,
|
||||
normalizeAgentDefaultsForJoin,
|
||||
} from "../routes/access.js";
|
||||
|
||||
describe("buildJoinDefaultsPayloadForAccept", () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -330,6 +335,13 @@ function parseBooleanLike(value: unknown): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function generateEd25519PrivateKeyPem(): string {
|
||||
const generated = generateKeyPairSync("ed25519");
|
||||
return generated.privateKey
|
||||
.export({ type: "pkcs8", format: "pem" })
|
||||
.toString();
|
||||
}
|
||||
|
||||
export function buildJoinDefaultsPayloadForAccept(input: {
|
||||
adapterType: string | null;
|
||||
defaultsPayload: unknown;
|
||||
@@ -611,10 +623,16 @@ function summarizeOpenClawGatewayDefaultsForLog(defaultsPayload: unknown) {
|
||||
sessionKeyStrategy: defaults
|
||||
? nonEmptyTrimmedString(defaults.sessionKeyStrategy)
|
||||
: null,
|
||||
disableDeviceAuth: defaults
|
||||
? parseBooleanLike(defaults.disableDeviceAuth)
|
||||
: null,
|
||||
waitTimeoutMs:
|
||||
defaults && typeof defaults.waitTimeoutMs === "number"
|
||||
? defaults.waitTimeoutMs
|
||||
: null,
|
||||
devicePrivateKeyPem: defaults
|
||||
? summarizeSecretForLog(defaults.devicePrivateKeyPem)
|
||||
: null,
|
||||
gatewayToken: summarizeSecretForLog(gatewayTokenValue)
|
||||
};
|
||||
}
|
||||
@@ -692,7 +710,7 @@ function buildJoinConnectivityDiagnostics(input: {
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function normalizeAgentDefaultsForJoin(input: {
|
||||
export function normalizeAgentDefaultsForJoin(input: {
|
||||
adapterType: string | null;
|
||||
defaultsPayload: unknown;
|
||||
deploymentMode: DeploymentMode;
|
||||
@@ -828,10 +846,47 @@ function normalizeAgentDefaultsForJoin(input: {
|
||||
}
|
||||
|
||||
const parsedDisableDeviceAuth = parseBooleanLike(defaults.disableDeviceAuth);
|
||||
const disableDeviceAuth = parsedDisableDeviceAuth === true;
|
||||
if (parsedDisableDeviceAuth !== null) {
|
||||
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 =
|
||||
typeof defaults.waitTimeoutMs === "number" &&
|
||||
Number.isFinite(defaults.waitTimeoutMs)
|
||||
@@ -1293,7 +1348,7 @@ function buildInviteOnboardingManifest(
|
||||
adapterType: "Use 'openclaw_gateway' for OpenClaw Gateway agents",
|
||||
capabilities: "Optional capability summary",
|
||||
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: {
|
||||
method: "POST",
|
||||
@@ -1430,7 +1485,6 @@ export function buildInviteOnboardingTextDocument(
|
||||
waitTimeoutMs: 120000,
|
||||
sessionKeyStrategy: "fixed",
|
||||
sessionKey: "paperclip",
|
||||
disableDeviceAuth: true,
|
||||
role: "operator",
|
||||
scopes: ["operator.admin"]
|
||||
}
|
||||
@@ -1447,8 +1501,9 @@ export function buildInviteOnboardingTextDocument(
|
||||
Legacy x-openclaw-auth is also accepted, but x-openclaw-token is preferred.
|
||||
Use adapterType "openclaw_gateway" and a ws:// or wss:// gateway URL.
|
||||
Pairing mode requirement:
|
||||
- For smoke/dev, set "disableDeviceAuth": true to avoid interactive pairing blocks.
|
||||
- If device auth remains enabled, set a stable "devicePrivateKeyPem"; otherwise each run may generate a new device identity and trigger pairing again.
|
||||
- Keep device auth enabled (recommended). If devicePrivateKeyPem is omitted, Paperclip generates and persists one during join so pairing approvals are stable.
|
||||
- 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.
|
||||
|
||||
Body (JSON):
|
||||
@@ -1464,7 +1519,6 @@ export function buildInviteOnboardingTextDocument(
|
||||
"waitTimeoutMs": 120000,
|
||||
"sessionKeyStrategy": "fixed",
|
||||
"sessionKey": "paperclip",
|
||||
"disableDeviceAuth": true,
|
||||
"role": "operator",
|
||||
"scopes": ["operator.admin"]
|
||||
}
|
||||
|
||||
@@ -486,8 +486,8 @@ When you submit the join request, use:
|
||||
- \`agentDefaultsPayload.url\` as your \`ws://\` or \`wss://\` gateway URL
|
||||
- \`agentDefaultsPayload.headers["x-openclaw-token"]\` with your gateway token
|
||||
- (legacy accepted) \`agentDefaultsPayload.headers["x-openclaw-auth"]\`
|
||||
- For stock smoke/dev onboarding: set \`agentDefaultsPayload.disableDeviceAuth = true\` to avoid repeated pairing prompts.
|
||||
- If keeping device auth enabled, provide a stable \`agentDefaultsPayload.devicePrivateKeyPem\`; otherwise a new ephemeral device ID may require pairing every run.
|
||||
- Keep device auth enabled (recommended). If \`devicePrivateKeyPem\` is omitted, Paperclip will generate and persist one during join so pairing approvals remain stable.
|
||||
- Only use \`disableDeviceAuth=true\` for special environments where pairing cannot be completed.
|
||||
|
||||
Do NOT use \`/v1/responses\` or \`/hooks/*\` in this join flow.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user