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 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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user