fix(openclaw-gateway): enforce join token validation and add smoke preflight gates

This commit is contained in:
Dotta
2026-03-07 16:01:19 -06:00
parent 271a632f1c
commit 83488b4ed0
6 changed files with 456 additions and 30 deletions

View File

@@ -25,29 +25,41 @@ Open the printed `Dashboard URL` (includes `#token=...`) in your browser.
5. Approve the join request in Paperclip UI, then confirm the OpenClaw agent appears in CLA agents.
6. Case A (manual issue test).
6. Gateway preflight (required before task tests).
- Confirm the created agent uses `openclaw_gateway` (not `openclaw`).
- Confirm gateway URL is `ws://...` or `wss://...`.
- Confirm gateway token is non-trivial (not empty / not 1-char placeholder).
- 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)}}'
```
- Expected: `adapterType=openclaw_gateway` and `tokenLen >= 16`.
7. Case A (manual issue test).
- Create an issue assigned to the OpenClaw agent.
- Put instructions: “post comment `OPENCLAW_CASE_A_OK_<timestamp>` and mark done.”
- Verify in UI: issue status becomes `done` and comment exists.
7. Case B (message tool test).
8. Case B (message tool test).
- Create another issue assigned to OpenClaw.
- Instructions: “send `OPENCLAW_CASE_B_OK_<timestamp>` to main webchat via message tool, then comment same marker on issue, then mark done.”
- Verify both:
- marker comment on issue
- marker text appears in OpenClaw main chat
8. Case C (new session memory/skills test).
9. Case C (new session memory/skills test).
- In OpenClaw, start `/new` session.
- Ask it to create a new CLA issue in Paperclip with unique title `OPENCLAW_CASE_C_CREATED_<timestamp>`.
- Verify in Paperclip UI that new issue exists.
9. Watch logs during test (optional but helpful):
10. Watch logs during test (optional but helpful):
```bash
docker compose -f /tmp/openclaw-docker/docker-compose.yml -f /tmp/openclaw-docker/.paperclip-openclaw.override.yml logs -f openclaw-gateway
```
10. Expected pass criteria.
11. Expected pass criteria.
- Preflight: `openclaw_gateway` + non-placeholder token (`tokenLen >= 16`).
- 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.

View File

@@ -78,26 +78,27 @@ Implication:
## Deep Code Findings (Gaps)
### 1) Onboarding content is still OpenClaw-HTTP specific
`server/src/routes/access.ts` hardcodes onboarding to:
- `recommendedAdapterType: "openclaw"`
- Required `agentDefaultsPayload.headers.x-openclaw-auth`
- HTTP callback URL guidance and `/v1/responses` examples.
### 1) Onboarding manifest/text gateway path (resolved)
Resolved in `server/src/routes/access.ts`:
- `recommendedAdapterType` now points to `openclaw_gateway`.
- Onboarding examples now require `adapterType: "openclaw_gateway"` + `ws://`/`wss://` URL + gateway token header.
- Added fail-fast guidance for short/placeholder tokens.
There is no adapter-specific onboarding manifest/text for `openclaw_gateway`.
### 2) Company settings snippet gateway path (resolved)
Resolved in `ui/src/pages/CompanySettings.tsx`:
- Snippet now instructs OpenClaw Gateway onboarding.
- Snippet explicitly says not to use `/v1/responses` or `/hooks/*` for this flow.
### 2) Company settings snippet is OpenClaw HTTP-first
`ui/src/pages/CompanySettings.tsx` generates one snippet that:
- Assumes OpenClaw HTTP callback setup.
- Instructs enabling `gateway.http.endpoints.responses.enabled=true`.
- Does not provide a dedicated gateway onboarding path.
### 3) Invite landing “agent join” UX is not wired for OpenClaw adapters
### 3) Invite landing “agent join” UX is not wired for OpenClaw adapters (open)
`ui/src/pages/InviteLanding.tsx` shows `openclaw` and `openclaw_gateway` as disabled (“Coming soon”) in join UI.
### 4) Join normalization/replay logic only special-cases `adapterType === "openclaw"`
`server/src/routes/access.ts` helper paths (`buildJoinDefaultsPayloadForAccept`, replay, normalization diagnostics) are OpenClaw-HTTP specific.
No equivalent normalization/diagnostics for gateway defaults.
### 4) Join normalization/replay logic parity (partially resolved)
Resolved:
- `buildJoinDefaultsPayloadForAccept` now normalizes wrapped gateway token headers for `openclaw_gateway`.
- `normalizeAgentDefaultsForJoin` now validates `openclaw_gateway` URL/token and rejects short placeholder tokens at invite-accept time.
Still open:
- Invite replay path is still special-cased to legacy `openclaw` joins.
### 5) Webhook confusion is expected in current setup
For `openclaw` + `streamTransport=webhook`:
@@ -257,11 +258,16 @@ POST /api/companies/$CLA_COMPANY_ID/invites
```
3. Approve join request.
4. Claim API key with `claimSecret`.
5. 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.
4. **Hard gate before any task run:** fetch created agent config and validate:
- `adapterType == "openclaw_gateway"`
- `adapterConfig.url` uses `ws://` or `wss://`
- `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
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.
- Write compatibility JSON keys (`token` and `apiKey`) to avoid runtime parser mismatch.
6. Ensure Paperclip skill is installed for OpenClaw runtime.
7. 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.
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.
## 6) E2E Validation Cases
@@ -311,6 +317,7 @@ Responsibilities:
- CLA company resolution.
- Old OpenClaw agent cleanup.
- Invite/join/approve/claim orchestration.
- Gateway agent config/token preflight validation before connectivity or case execution.
- E2E case execution + assertions.
- Final summary with run IDs, issue IDs, agent ID.
@@ -347,5 +354,6 @@ Responsibilities:
## Acceptance Criteria
- No webhook-mode ambiguity: webhook path does not silently appear as SSE success without explicit compatibility signal.
- Gateway onboarding is first-class and copy/pasteable from company settings.
- Gateway join fails fast if token is missing/placeholder, and smoke preflight verifies adapter/token parity before task runs.
- Codex can run end-to-end onboarding and validation against CLA with repeatable cleanup.
- All three validation cases are documented with pass/fail criteria and reproducible evidence paths.

View File

@@ -524,6 +524,34 @@ inject_agent_api_key_payload_template() {
assert_status "200"
}
validate_joined_gateway_agent() {
local expected_gateway_token="$1"
api_request "GET" "/agents/${AGENT_ID}"
assert_status "200"
local adapter_type gateway_url configured_token
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")"
[[ "$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}'"
[[ -n "$configured_token" ]] || fail "joined agent missing adapterConfig.headers.x-openclaw-token"
if (( ${#configured_token} < 16 )); then
fail "joined agent gateway token looks too short (${#configured_token} chars)"
fi
local expected_hash configured_hash
expected_hash="$(hash_prefix "$expected_gateway_token")"
configured_hash="$(hash_prefix "$configured_token")"
if [[ "$expected_hash" != "$configured_hash" ]]; then
fail "joined agent gateway token hash mismatch (expected ${expected_hash}, got ${configured_hash})"
fi
log "validated joined gateway agent config (token sha256 prefix ${configured_hash})"
}
trigger_wakeup() {
local reason="$1"
local issue_id="${2:-}"
@@ -840,6 +868,7 @@ main() {
create_and_approve_gateway_join "$gateway_token"
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

View File

@@ -208,4 +208,41 @@ describe("buildJoinDefaultsPayloadForAccept", () => {
expect(result).toEqual(defaultsPayload);
});
it("normalizes wrapped gateway token headers for openclaw_gateway", () => {
const result = buildJoinDefaultsPayloadForAccept({
adapterType: "openclaw_gateway",
defaultsPayload: {
url: "ws://127.0.0.1:18789",
headers: {
"x-openclaw-token": {
value: "gateway-token-1234567890",
},
},
},
}) as Record<string, unknown>;
expect(result).toMatchObject({
url: "ws://127.0.0.1:18789",
headers: {
"x-openclaw-token": "gateway-token-1234567890",
},
});
});
it("accepts inbound x-openclaw-token for openclaw_gateway", () => {
const result = buildJoinDefaultsPayloadForAccept({
adapterType: "openclaw_gateway",
defaultsPayload: {
url: "ws://127.0.0.1:18789",
},
inboundOpenClawTokenHeader: "gateway-token-1234567890",
}) as Record<string, unknown>;
expect(result).toMatchObject({
headers: {
"x-openclaw-token": "gateway-token-1234567890",
},
});
});
});

View File

@@ -52,6 +52,7 @@ describe("buildInviteOnboardingTextDocument", () => {
expect(text).toContain("~/.openclaw/workspace/paperclip-claimed-api-key.json");
expect(text).toContain("PAPERCLIP_API_KEY");
expect(text).toContain("saved token field");
expect(text).toContain("Gateway token unexpectedly short");
});
it("includes loopback diagnostics for authenticated/private onboarding", () => {

View File

@@ -311,6 +311,25 @@ function toAuthorizationHeaderValue(rawToken: string): string {
return /^bearer\s+/i.test(trimmed) ? trimmed : `Bearer ${trimmed}`;
}
function tokenFromAuthorizationHeader(rawHeader: string | null): string | null {
const trimmed = nonEmptyTrimmedString(rawHeader);
if (!trimmed) return null;
const bearerMatch = trimmed.match(/^bearer\s+(.+)$/i);
if (bearerMatch?.[1]) {
return nonEmptyTrimmedString(bearerMatch[1]);
}
return trimmed;
}
function parseBooleanLike(value: unknown): boolean | null {
if (typeof value === "boolean") return value;
if (typeof value !== "string") return null;
const normalized = value.trim().toLowerCase();
if (normalized === "true" || normalized === "1") return true;
if (normalized === "false" || normalized === "0") return false;
return null;
}
export function buildJoinDefaultsPayloadForAccept(input: {
adapterType: string | null;
defaultsPayload: unknown;
@@ -322,6 +341,59 @@ export function buildJoinDefaultsPayloadForAccept(input: {
inboundOpenClawAuthHeader?: string | null;
inboundOpenClawTokenHeader?: string | null;
}): unknown {
if (input.adapterType === "openclaw_gateway") {
const merged = isPlainObject(input.defaultsPayload)
? { ...(input.defaultsPayload as Record<string, unknown>) }
: ({} as Record<string, unknown>);
if (!nonEmptyTrimmedString(merged.paperclipApiUrl)) {
const legacyPaperclipApiUrl = nonEmptyTrimmedString(input.paperclipApiUrl);
if (legacyPaperclipApiUrl) merged.paperclipApiUrl = legacyPaperclipApiUrl;
}
const mergedHeaders = normalizeHeaderMap(merged.headers) ?? {};
const inboundOpenClawAuthHeader = nonEmptyTrimmedString(
input.inboundOpenClawAuthHeader
);
const inboundOpenClawTokenHeader = nonEmptyTrimmedString(
input.inboundOpenClawTokenHeader
);
if (
inboundOpenClawTokenHeader &&
!headerMapHasKeyIgnoreCase(mergedHeaders, "x-openclaw-token")
) {
mergedHeaders["x-openclaw-token"] = inboundOpenClawTokenHeader;
}
if (
inboundOpenClawAuthHeader &&
!headerMapHasKeyIgnoreCase(mergedHeaders, "x-openclaw-auth")
) {
mergedHeaders["x-openclaw-auth"] = inboundOpenClawAuthHeader;
}
const discoveredToken =
headerMapGetIgnoreCase(mergedHeaders, "x-openclaw-token") ??
headerMapGetIgnoreCase(mergedHeaders, "x-openclaw-auth") ??
tokenFromAuthorizationHeader(
headerMapGetIgnoreCase(mergedHeaders, "authorization")
);
if (
discoveredToken &&
!headerMapHasKeyIgnoreCase(mergedHeaders, "x-openclaw-token")
) {
mergedHeaders["x-openclaw-token"] = discoveredToken;
}
if (Object.keys(mergedHeaders).length > 0) {
merged.headers = mergedHeaders;
} else {
delete merged.headers;
}
return Object.keys(merged).length > 0 ? merged : null;
}
if (input.adapterType !== "openclaw") {
return input.defaultsPayload;
}
@@ -516,6 +588,37 @@ function summarizeOpenClawDefaultsForLog(defaultsPayload: unknown) {
};
}
function summarizeOpenClawGatewayDefaultsForLog(defaultsPayload: unknown) {
const defaults = isPlainObject(defaultsPayload)
? (defaultsPayload as Record<string, unknown>)
: null;
const headers = defaults ? normalizeHeaderMap(defaults.headers) : undefined;
const gatewayTokenValue = headers
? headerMapGetIgnoreCase(headers, "x-openclaw-token") ??
headerMapGetIgnoreCase(headers, "x-openclaw-auth") ??
tokenFromAuthorizationHeader(
headerMapGetIgnoreCase(headers, "authorization")
)
: null;
return {
present: Boolean(defaults),
keys: defaults ? Object.keys(defaults).sort() : [],
url: defaults ? nonEmptyTrimmedString(defaults.url) : null,
paperclipApiUrl: defaults
? nonEmptyTrimmedString(defaults.paperclipApiUrl)
: null,
headerKeys: headers ? Object.keys(headers).sort() : [],
sessionKeyStrategy: defaults
? nonEmptyTrimmedString(defaults.sessionKeyStrategy)
: null,
waitTimeoutMs:
defaults && typeof defaults.waitTimeoutMs === "number"
? defaults.waitTimeoutMs
: null,
gatewayToken: summarizeSecretForLog(gatewayTokenValue)
};
}
function buildJoinConnectivityDiagnostics(input: {
deploymentMode: DeploymentMode;
deploymentExposure: DeploymentExposure;
@@ -597,12 +700,222 @@ function normalizeAgentDefaultsForJoin(input: {
bindHost: string;
allowedHostnames: string[];
}) {
const fatalErrors: string[] = [];
const diagnostics: JoinDiagnostic[] = [];
if (input.adapterType !== "openclaw") {
if (
input.adapterType !== "openclaw" &&
input.adapterType !== "openclaw_gateway"
) {
const normalized = isPlainObject(input.defaultsPayload)
? (input.defaultsPayload as Record<string, unknown>)
: null;
return { normalized, diagnostics };
return { normalized, diagnostics, fatalErrors };
}
if (input.adapterType === "openclaw_gateway") {
if (!isPlainObject(input.defaultsPayload)) {
diagnostics.push({
code: "openclaw_gateway_defaults_missing",
level: "warn",
message:
"No OpenClaw gateway config was provided in agentDefaultsPayload.",
hint:
"Include agentDefaultsPayload.url and headers.x-openclaw-token for OpenClaw gateway joins."
});
fatalErrors.push(
"agentDefaultsPayload is required for adapterType=openclaw_gateway"
);
return {
normalized: null as Record<string, unknown> | null,
diagnostics,
fatalErrors
};
}
const defaults = input.defaultsPayload as Record<string, unknown>;
const normalized: Record<string, unknown> = {};
let gatewayUrl: URL | null = null;
const rawGatewayUrl = nonEmptyTrimmedString(defaults.url);
if (!rawGatewayUrl) {
diagnostics.push({
code: "openclaw_gateway_url_missing",
level: "warn",
message: "OpenClaw gateway URL is missing.",
hint: "Set agentDefaultsPayload.url to ws:// or wss:// gateway URL."
});
fatalErrors.push("agentDefaultsPayload.url is required");
} else {
try {
gatewayUrl = new URL(rawGatewayUrl);
if (
gatewayUrl.protocol !== "ws:" &&
gatewayUrl.protocol !== "wss:"
) {
diagnostics.push({
code: "openclaw_gateway_url_protocol",
level: "warn",
message: `OpenClaw gateway URL must use ws:// or wss:// (got ${gatewayUrl.protocol}).`
});
fatalErrors.push(
"agentDefaultsPayload.url must use ws:// or wss:// for openclaw_gateway"
);
} else {
normalized.url = gatewayUrl.toString();
diagnostics.push({
code: "openclaw_gateway_url_configured",
level: "info",
message: `Gateway endpoint set to ${gatewayUrl.toString()}`
});
}
} catch {
diagnostics.push({
code: "openclaw_gateway_url_invalid",
level: "warn",
message: `Invalid OpenClaw gateway URL: ${rawGatewayUrl}`
});
fatalErrors.push("agentDefaultsPayload.url is not a valid URL");
}
}
const headers = normalizeHeaderMap(defaults.headers) ?? {};
const gatewayToken =
headerMapGetIgnoreCase(headers, "x-openclaw-token") ??
headerMapGetIgnoreCase(headers, "x-openclaw-auth") ??
tokenFromAuthorizationHeader(headerMapGetIgnoreCase(headers, "authorization"));
if (
gatewayToken &&
!headerMapHasKeyIgnoreCase(headers, "x-openclaw-token")
) {
headers["x-openclaw-token"] = gatewayToken;
}
if (Object.keys(headers).length > 0) {
normalized.headers = headers;
}
if (!gatewayToken) {
diagnostics.push({
code: "openclaw_gateway_auth_header_missing",
level: "warn",
message: "Gateway auth token is missing from agent defaults.",
hint:
"Set agentDefaultsPayload.headers.x-openclaw-token (or legacy x-openclaw-auth)."
});
fatalErrors.push(
"agentDefaultsPayload.headers.x-openclaw-token (or x-openclaw-auth) is required"
);
} else if (gatewayToken.trim().length < 16) {
diagnostics.push({
code: "openclaw_gateway_auth_header_too_short",
level: "warn",
message: `Gateway auth token appears too short (${gatewayToken.trim().length} chars).`,
hint:
"Use the full gateway auth token from ~/.openclaw/openclaw.json (typically long random string)."
});
fatalErrors.push(
"agentDefaultsPayload.headers.x-openclaw-token is too short; expected a full gateway token"
);
} else {
diagnostics.push({
code: "openclaw_gateway_auth_header_configured",
level: "info",
message: "Gateway auth token configured."
});
}
if (isPlainObject(defaults.payloadTemplate)) {
normalized.payloadTemplate = defaults.payloadTemplate;
}
const parsedDisableDeviceAuth = parseBooleanLike(defaults.disableDeviceAuth);
if (parsedDisableDeviceAuth !== null) {
normalized.disableDeviceAuth = parsedDisableDeviceAuth;
}
const waitTimeoutMs =
typeof defaults.waitTimeoutMs === "number" &&
Number.isFinite(defaults.waitTimeoutMs)
? Math.floor(defaults.waitTimeoutMs)
: typeof defaults.waitTimeoutMs === "string"
? Number.parseInt(defaults.waitTimeoutMs.trim(), 10)
: NaN;
if (Number.isFinite(waitTimeoutMs) && waitTimeoutMs > 0) {
normalized.waitTimeoutMs = waitTimeoutMs;
}
const timeoutSec =
typeof defaults.timeoutSec === "number" && Number.isFinite(defaults.timeoutSec)
? Math.floor(defaults.timeoutSec)
: typeof defaults.timeoutSec === "string"
? Number.parseInt(defaults.timeoutSec.trim(), 10)
: NaN;
if (Number.isFinite(timeoutSec) && timeoutSec > 0) {
normalized.timeoutSec = timeoutSec;
}
const sessionKeyStrategy = nonEmptyTrimmedString(defaults.sessionKeyStrategy);
if (
sessionKeyStrategy === "fixed" ||
sessionKeyStrategy === "issue" ||
sessionKeyStrategy === "run"
) {
normalized.sessionKeyStrategy = sessionKeyStrategy;
}
const sessionKey = nonEmptyTrimmedString(defaults.sessionKey);
if (sessionKey) {
normalized.sessionKey = sessionKey;
}
const role = nonEmptyTrimmedString(defaults.role);
if (role) {
normalized.role = role;
}
if (Array.isArray(defaults.scopes)) {
const scopes = defaults.scopes
.filter((entry): entry is string => typeof entry === "string")
.map((entry) => entry.trim())
.filter(Boolean);
if (scopes.length > 0) {
normalized.scopes = scopes;
}
}
const rawPaperclipApiUrl =
typeof defaults.paperclipApiUrl === "string"
? defaults.paperclipApiUrl.trim()
: "";
if (rawPaperclipApiUrl) {
try {
const parsedPaperclipApiUrl = new URL(rawPaperclipApiUrl);
if (
parsedPaperclipApiUrl.protocol !== "http:" &&
parsedPaperclipApiUrl.protocol !== "https:"
) {
diagnostics.push({
code: "openclaw_gateway_paperclip_api_url_protocol",
level: "warn",
message: `paperclipApiUrl must use http:// or https:// (got ${parsedPaperclipApiUrl.protocol}).`
});
} else {
normalized.paperclipApiUrl = parsedPaperclipApiUrl.toString();
diagnostics.push({
code: "openclaw_gateway_paperclip_api_url_configured",
level: "info",
message: `paperclipApiUrl set to ${parsedPaperclipApiUrl.toString()}`
});
}
} catch {
diagnostics.push({
code: "openclaw_gateway_paperclip_api_url_invalid",
level: "warn",
message: `Invalid paperclipApiUrl: ${rawPaperclipApiUrl}`
});
}
}
return { normalized, diagnostics, fatalErrors };
}
if (!isPlainObject(input.defaultsPayload)) {
@@ -613,7 +926,11 @@ function normalizeAgentDefaultsForJoin(input: {
"No OpenClaw callback config was provided in agentDefaultsPayload.",
hint: "Include agentDefaultsPayload.url so Paperclip can invoke the OpenClaw endpoint immediately after approval."
});
return { normalized: null as Record<string, unknown> | null, diagnostics };
return {
normalized: null as Record<string, unknown> | null,
diagnostics,
fatalErrors
};
}
const defaults = input.defaultsPayload as Record<string, unknown>;
@@ -790,7 +1107,7 @@ function normalizeAgentDefaultsForJoin(input: {
})
);
return { normalized, diagnostics };
return { normalized, diagnostics, fatalErrors };
}
function toInviteSummaryResponse(
@@ -1091,6 +1408,7 @@ export function buildInviteOnboardingTextDocument(
TOKEN="$(node -p 'require(process.env.HOME+\"/.openclaw/openclaw.json\").gateway.auth.token')"
test -n "$TOKEN" || (echo "Missing TOKEN" && exit 1)
test "\${#TOKEN}" -ge 16 || (echo "Gateway token unexpectedly short (\${#TOKEN})" && exit 1)
3) IMPORTANT: Don't accidentally drop the token when generating JSON
If you build JSON with Node, pass the token explicitly (argv), don't rely on an un-exported env var.
@@ -1931,9 +2249,14 @@ export function accessRoutes(
})
: {
normalized: null as Record<string, unknown> | null,
diagnostics: [] as JoinDiagnostic[]
diagnostics: [] as JoinDiagnostic[],
fatalErrors: [] as string[]
};
if (requestType === "agent" && joinDefaults.fatalErrors.length > 0) {
throw badRequest(joinDefaults.fatalErrors.join("; "));
}
if (requestType === "agent" && adapterType === "openclaw") {
logger.info(
{
@@ -1950,6 +2273,22 @@ export function accessRoutes(
);
}
if (requestType === "agent" && adapterType === "openclaw_gateway") {
logger.info(
{
inviteId: invite.id,
joinRequestDiagnostics: joinDefaults.diagnostics.map((diag) => ({
code: diag.code,
level: diag.level
})),
normalizedAgentDefaults: summarizeOpenClawGatewayDefaultsForLog(
joinDefaults.normalized
)
},
"invite accept normalized OpenClaw gateway defaults"
);
}
const claimSecret =
requestType === "agent" && !inviteAlreadyAccepted
? createClaimSecret()