@elizaos/plugin-elizacloud 2.0.3-beta.6 → 2.0.3-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cjs/index.node.cjs +155 -76
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/cloud-providers/cloud-status.d.ts.map +1 -1
- package/dist/cloud-providers/cloud-status.js +4 -1
- package/dist/cloud-providers/cloud-status.js.map +3 -3
- package/dist/index.js +94 -16
- package/dist/index.js.map +4 -4
- package/dist/node/index.node.js +95 -16
- package/dist/node/index.node.js.map +4 -4
- package/dist/services/cloud-auth.d.ts +61 -1
- package/dist/services/cloud-auth.d.ts.map +1 -1
- package/dist/services/cloud-auth.js +93 -16
- package/dist/services/cloud-auth.js.map +3 -3
- package/package.json +5 -5
- package/src/cloud-providers/cloud-status.ts +6 -0
- package/src/services/cloud-auth.ts +180 -26
|
@@ -26,11 +26,10 @@ import {
|
|
|
26
26
|
resolveApiSecurityConfig,
|
|
27
27
|
resolveDesktopApiPort,
|
|
28
28
|
} from "@elizaos/core";
|
|
29
|
-
import { isCloudReachable } from "@elizaos/shared";
|
|
30
29
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
31
30
|
import type { CloudCredentials, DeviceAuthResponse, DevicePlatform } from "../types/cloud";
|
|
32
31
|
import { DEFAULT_CLOUD_CONFIG } from "../types/cloud";
|
|
33
|
-
import { CloudApiClient } from "../utils/cloud-api";
|
|
32
|
+
import { CloudApiClient, CloudApiError } from "../utils/cloud-api";
|
|
34
33
|
import type { CloudBootstrapService } from "./cloud-bootstrap";
|
|
35
34
|
|
|
36
35
|
/** SHA-256 hash of hostname + platform + arch + cpu + memory. */
|
|
@@ -389,12 +388,115 @@ export async function exchangeCodeForSession(args: ExchangeCodeArgs): Promise<Cl
|
|
|
389
388
|
|
|
390
389
|
// ─── Service ───────────────────────────────────────────────────────────────
|
|
391
390
|
|
|
391
|
+
/** Result of probing the saved API key against the cloud. */
|
|
392
|
+
export type ApiKeyProbe = "valid" | "unreachable" | "invalid";
|
|
393
|
+
|
|
394
|
+
export interface RevalidationState {
|
|
395
|
+
/** `unknown` until a probe resolves; `invalid` once a revoked key is confirmed. */
|
|
396
|
+
keyState: "unknown" | "valid" | "invalid";
|
|
397
|
+
/** Consecutive reachable-but-rejected probes; debounces a transient 5xx. */
|
|
398
|
+
consecutiveInvalid: number;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface RevalidationConfig {
|
|
402
|
+
/** Re-probe delay while the key state is unresolved (unreachable / unconfirmed-invalid). */
|
|
403
|
+
retryMs: number;
|
|
404
|
+
/** Re-probe delay once the key state is resolved — catches a LATER revocation. */
|
|
405
|
+
steadyMs: number;
|
|
406
|
+
/** Reachable-but-rejected probes required before declaring the key revoked. */
|
|
407
|
+
invalidThreshold: number;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export interface RevalidationDecision {
|
|
411
|
+
state: RevalidationState;
|
|
412
|
+
delayMs: number;
|
|
413
|
+
log: { level: "info" | "error"; message: string } | null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export const DEFAULT_REVALIDATION_CONFIG: RevalidationConfig = {
|
|
417
|
+
retryMs: 60_000,
|
|
418
|
+
steadyMs: 30 * 60_000,
|
|
419
|
+
invalidThreshold: 2,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Pure state machine for background API-key re-validation. Given the current
|
|
424
|
+
* state and a fresh probe result, decide the next state, when to re-probe, and
|
|
425
|
+
* whether to emit a one-shot state-change log. No I/O — fully unit-testable.
|
|
426
|
+
*
|
|
427
|
+
* Why this exists: at boot the key is trusted optimistically and validated once
|
|
428
|
+
* in the background. If the cloud was unreachable at boot (or the key is revoked
|
|
429
|
+
* AFTER boot), the one-shot check left the agent 401-blind on every turn with no
|
|
430
|
+
* surfaced state. This loop retries transient unreachability, confirms a revoked
|
|
431
|
+
* key with a loud actionable error (debounced so a single 5xx doesn't false-
|
|
432
|
+
* alarm), and steady-re-checks so a post-boot revocation is still caught and a
|
|
433
|
+
* later re-authorization self-heals.
|
|
434
|
+
*/
|
|
435
|
+
export function decideRevalidation(
|
|
436
|
+
prev: RevalidationState,
|
|
437
|
+
probe: ApiKeyProbe,
|
|
438
|
+
cfg: RevalidationConfig = DEFAULT_REVALIDATION_CONFIG
|
|
439
|
+
): RevalidationDecision {
|
|
440
|
+
if (probe === "valid") {
|
|
441
|
+
const log =
|
|
442
|
+
prev.keyState !== "valid"
|
|
443
|
+
? { level: "info" as const, message: "[CloudAuth] API key validated" }
|
|
444
|
+
: null;
|
|
445
|
+
return {
|
|
446
|
+
state: { keyState: "valid", consecutiveInvalid: 0 },
|
|
447
|
+
delayMs: cfg.steadyMs,
|
|
448
|
+
log,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (probe === "invalid") {
|
|
453
|
+
const consecutiveInvalid = prev.consecutiveInvalid + 1;
|
|
454
|
+
const confirmed = consecutiveInvalid >= cfg.invalidThreshold;
|
|
455
|
+
if (confirmed) {
|
|
456
|
+
const log =
|
|
457
|
+
prev.keyState !== "invalid"
|
|
458
|
+
? {
|
|
459
|
+
level: "error" as const,
|
|
460
|
+
message:
|
|
461
|
+
"[CloudAuth] Eliza Cloud API key is REVOKED/INVALID (cloud reachable, key rejected). " +
|
|
462
|
+
"Model calls will fail with 401 until the agent is re-provisioned or a valid ELIZAOS_CLOUD_API_KEY is set.",
|
|
463
|
+
}
|
|
464
|
+
: null;
|
|
465
|
+
return {
|
|
466
|
+
state: { keyState: "invalid", consecutiveInvalid },
|
|
467
|
+
delayMs: cfg.steadyMs,
|
|
468
|
+
log,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
// Not yet confirmed — re-probe soon (don't change the surfaced state yet).
|
|
472
|
+
return {
|
|
473
|
+
state: { keyState: prev.keyState, consecutiveInvalid },
|
|
474
|
+
delayMs: cfg.retryMs,
|
|
475
|
+
log: null,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// unreachable — transient network state, not an auth signal: keep the prior
|
|
480
|
+
// state and back off. (consecutiveInvalid is preserved, not reset, so a blip
|
|
481
|
+
// between two real rejections doesn't restart the confirmation count.)
|
|
482
|
+
return {
|
|
483
|
+
state: { ...prev },
|
|
484
|
+
delayMs: cfg.retryMs,
|
|
485
|
+
log: null,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
392
489
|
export class CloudAuthService extends Service {
|
|
393
490
|
static serviceType = "CLOUD_AUTH";
|
|
394
491
|
capabilityDescription = "Eliza Cloud device authentication and SSO session helpers";
|
|
395
492
|
|
|
396
493
|
private client: CloudApiClient;
|
|
397
494
|
private credentials: CloudCredentials | null = null;
|
|
495
|
+
private revalidationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
496
|
+
private revalidationState: RevalidationState = {
|
|
497
|
+
keyState: "unknown",
|
|
498
|
+
consecutiveInvalid: 0,
|
|
499
|
+
};
|
|
398
500
|
|
|
399
501
|
constructor(runtime?: IAgentRuntime) {
|
|
400
502
|
super(runtime);
|
|
@@ -408,6 +510,11 @@ export class CloudAuthService extends Service {
|
|
|
408
510
|
}
|
|
409
511
|
|
|
410
512
|
async stop(): Promise<void> {
|
|
513
|
+
if (this.revalidationTimer) {
|
|
514
|
+
clearTimeout(this.revalidationTimer);
|
|
515
|
+
this.revalidationTimer = null;
|
|
516
|
+
}
|
|
517
|
+
this.revalidationState = { keyState: "unknown", consecutiveInvalid: 0 };
|
|
411
518
|
this.credentials = null;
|
|
412
519
|
}
|
|
413
520
|
|
|
@@ -441,19 +548,13 @@ export class CloudAuthService extends Service {
|
|
|
441
548
|
};
|
|
442
549
|
logger.info("[CloudAuth] Authenticated with saved API key");
|
|
443
550
|
|
|
444
|
-
// Non-blocking
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
})
|
|
454
|
-
.catch(() => {
|
|
455
|
-
// Swallow — already logged inside validateApiKey
|
|
456
|
-
});
|
|
551
|
+
// Non-blocking, self-healing key re-validation. Boot stays instant; a
|
|
552
|
+
// background loop confirms the key — retrying transient cloud-unreachable
|
|
553
|
+
// so a boot-time outage doesn't leave it unvalidated forever, surfacing a
|
|
554
|
+
// loud actionable error the moment the key is confirmed revoked, and
|
|
555
|
+
// steady-re-checking so a revocation that happens AFTER boot is caught
|
|
556
|
+
// instead of the agent silently 401ing every turn (see #8434 key-flag).
|
|
557
|
+
this.scheduleRevalidation(0);
|
|
457
558
|
return;
|
|
458
559
|
}
|
|
459
560
|
|
|
@@ -474,22 +575,75 @@ export class CloudAuthService extends Service {
|
|
|
474
575
|
}
|
|
475
576
|
}
|
|
476
577
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
578
|
+
/**
|
|
579
|
+
* Probe the saved key: `valid` when an authenticated `/models` call succeeds,
|
|
580
|
+
* `invalid` ONLY when the cloud is reachable and explicitly rejects the key
|
|
581
|
+
* (401/403 — revoked/expired), `unreachable` for every other failure (5xx,
|
|
582
|
+
* 429, timeout, network error). Only a `CloudApiError` carries a real HTTP
|
|
583
|
+
* status; timeouts (`AbortSignal.timeout`) and connection failures reject with
|
|
584
|
+
* a raw fetch/DOMException and so are correctly treated as `unreachable`. A
|
|
585
|
+
* server-side 5xx/429 is NOT an auth signal — classifying it as `invalid`
|
|
586
|
+
* would false-alarm a revoked-key error on a transient outage. The `invalid`
|
|
587
|
+
* case is still debounced upstream so a single 401 blip can't flip the state.
|
|
588
|
+
*/
|
|
589
|
+
private async probeApiKey(key: string): Promise<ApiKeyProbe> {
|
|
484
590
|
try {
|
|
485
591
|
const validationClient = new CloudApiClient(this.client.getBaseUrl(), key);
|
|
486
592
|
await validationClient.get("/models", { timeoutMs: 2_500 });
|
|
487
|
-
return
|
|
593
|
+
return "valid";
|
|
488
594
|
} catch (err) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
595
|
+
if (err instanceof CloudApiError && (err.statusCode === 401 || err.statusCode === 403)) {
|
|
596
|
+
return "invalid";
|
|
597
|
+
}
|
|
598
|
+
return "unreachable";
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private scheduleRevalidation(delayMs: number): void {
|
|
603
|
+
if (this.revalidationTimer) {
|
|
604
|
+
clearTimeout(this.revalidationTimer);
|
|
492
605
|
}
|
|
606
|
+
// The timer is always cleared in stop(), so it can't outlive the service.
|
|
607
|
+
const timer = setTimeout(() => {
|
|
608
|
+
void this.runRevalidation();
|
|
609
|
+
}, delayMs);
|
|
610
|
+
// Don't let the background re-probe pin the Node event loop / block a clean
|
|
611
|
+
// process exit. `unref` is Node-only; it's absent on the browser shim type.
|
|
612
|
+
timer.unref?.();
|
|
613
|
+
this.revalidationTimer = timer;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private async runRevalidation(): Promise<void> {
|
|
617
|
+
const key = this.credentials?.apiKey;
|
|
618
|
+
if (!key) {
|
|
619
|
+
return; // logged out / stopped — let the loop die
|
|
620
|
+
}
|
|
621
|
+
const probe = await this.probeApiKey(key).catch(
|
|
622
|
+
() => "unreachable" as ApiKeyProbe
|
|
623
|
+
);
|
|
624
|
+
// The active key may have changed during the await (re-auth) — drop stale.
|
|
625
|
+
if (this.credentials?.apiKey !== key) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const decision = decideRevalidation(this.revalidationState, probe);
|
|
629
|
+
this.revalidationState = decision.state;
|
|
630
|
+
if (decision.log) {
|
|
631
|
+
if (decision.log.level === "error") {
|
|
632
|
+
logger.error(decision.log.message);
|
|
633
|
+
} else {
|
|
634
|
+
logger.info(decision.log.message);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
this.scheduleRevalidation(decision.delayMs);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* True once a background probe has CONFIRMED the saved API key is
|
|
642
|
+
* revoked/invalid (cloud reachable, key rejected). Status/health surfaces can
|
|
643
|
+
* read this to report a degraded agent instead of letting it 401 blindly.
|
|
644
|
+
*/
|
|
645
|
+
isApiKeyInvalid(): boolean {
|
|
646
|
+
return this.revalidationState.keyState === "invalid";
|
|
493
647
|
}
|
|
494
648
|
|
|
495
649
|
/**
|