Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plugins/plugin-browser/src/bridge-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export function browserBridgeDomainFromUrl(url: string): string | null {
const hostname = parsed.hostname.trim().toLowerCase().replace(/\.+$/, "");
return hostname.length > 0 ? hostname : null;
} catch {
// error-policy:J3 untrusted-input sanitizing — `new URL()` throws on a
// malformed URL; null is the explicit "not a valid http(s) URL" signal
// callers fail-closed on (no domain → no focus/policy match), never a
// fabricated-valid domain.
return null;
}
}
3 changes: 3 additions & 0 deletions plugins/plugin-browser/src/routes/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ function decodeBrowserWorkspaceTabId(raw: string | undefined): string | null {
const decoded = decodeURIComponent(raw).trim();
return decoded ? decoded : null;
} catch {
// error-policy:J3 untrusted-input sanitizing — decodeURIComponent throws on
// a malformed percent-encoding in a path param; null is the explicit
// "invalid tab id" signal (the route then 404s), never a fabricated id.
return null;
}
}
Expand Down
21 changes: 19 additions & 2 deletions plugins/plugin-browser/src/workspace/browser-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,30 @@ export async function stopBrowserCapture() {
if (activeCaptureLoop) {
try {
await activeCaptureLoop;
} catch {}
} catch (err) {
// error-policy:J6 best-effort teardown — the capture loop is being torn
// down; a late frame-capture rejection is already surfaced inside the loop
// (see logger.warn above) and must not block shutdown.
logger.debug(
`[browser-capture] capture loop settled with error during stop: ${
err instanceof Error ? err.message : String(err)
}`,
);
}
activeCaptureLoop = null;
}
if (activeBrowser) {
try {
await activeBrowser.close();
} catch {}
} catch (err) {
// error-policy:J6 best-effort teardown — a browser that fails to close
// cleanly during shutdown cannot be recovered here; drop the reference.
logger.debug(
`[browser-capture] browser close failed during stop: ${
err instanceof Error ? err.message : String(err)
}`,
);
}
activeBrowser = null;
}
logger.info("[browser-capture] Stopped");
Expand Down
83 changes: 83 additions & 0 deletions plugins/plugin-wallet/src/wallet/local-eoa-backend.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Unit tests for `LocalEoaBackend.create` key resolution. Exercises the real
* key-derivation path (no mocked signer): a valid base58 Solana secret yields a
* usable signer, and — critically — a configured-but-malformed key surfaces the
* typed `SolanaPrivateKeyInvalidError` instead of being swallowed into a null
* that reads identically to "no wallet configured".
*/
import type { IAgentRuntime } from "@elizaos/core";
import { Keypair } from "@solana/web3.js";
import bs58 from "bs58";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { LocalEoaBackend } from "./local-eoa-backend";
import {
SolanaPrivateKeyInvalidError,
WalletBackendNotConfiguredError,
} from "./errors";

const KEY_ENV_VARS = [
"EVM_PRIVATE_KEY",
"SOLANA_PRIVATE_KEY",
"WALLET_PRIVATE_KEY",
];

function runtimeWith(settings: Record<string, string | undefined>): IAgentRuntime {
return {
getSetting: vi.fn((key: string) => settings[key]),
} as unknown as IAgentRuntime;
}

describe("LocalEoaBackend.create — Solana key resolution", () => {
const savedEnv: Record<string, string | undefined> = {};

beforeEach(() => {
// The resolver falls back to process.env; clear the wallet keys so the
// test's runtime.getSetting is the sole source of truth.
for (const name of KEY_ENV_VARS) {
savedEnv[name] = process.env[name];
delete process.env[name];
}
});

afterEach(() => {
for (const name of KEY_ENV_VARS) {
if (savedEnv[name] === undefined) {
delete process.env[name];
} else {
process.env[name] = savedEnv[name];
}
}
});

it("derives a Solana signer from a valid base58 secret", async () => {
const kp = Keypair.generate();
const secret = bs58.encode(kp.secretKey);
const backend = await LocalEoaBackend.create(
runtimeWith({ SOLANA_PRIVATE_KEY: secret }),
);
expect(String(backend.getAddresses().solana)).toBe(kp.publicKey.toBase58());
expect(backend.canSign("solana")).toBe(true);
});

it("surfaces a malformed configured key instead of masking it as 'no wallet'", async () => {
// A configured key that decodes to the wrong length is a misconfiguration:
// it must throw the typed invalid-key error, never fall through to
// WalletBackendNotConfiguredError (which means "no key was configured").
const wrongLength = bs58.encode(new Uint8Array(48));
await expect(
LocalEoaBackend.create(runtimeWith({ SOLANA_PRIVATE_KEY: wrongLength })),
).rejects.toBeInstanceOf(SolanaPrivateKeyInvalidError);
});

it("surfaces a non-base58 configured key as a typed invalid-key error", async () => {
await expect(
LocalEoaBackend.create(runtimeWith({ SOLANA_PRIVATE_KEY: "not valid base58 !!!" })),
).rejects.toBeInstanceOf(SolanaPrivateKeyInvalidError);
});

it("still reports NO_WALLET_CONFIGURED when genuinely no key is set", async () => {
await expect(LocalEoaBackend.create(runtimeWith({}))).rejects.toBeInstanceOf(
WalletBackendNotConfiguredError,
);
});
});
10 changes: 5 additions & 5 deletions plugins/plugin-wallet/src/wallet/local-eoa-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,14 @@ function resolveSolanaKeypair(runtime: IAgentRuntime): Keypair | null {
process.env.SOLANA_PRIVATE_KEY ??
readSetting(runtime, "WALLET_PRIVATE_KEY") ??
process.env.WALLET_PRIVATE_KEY;
// Absence (no key configured) is a legitimate null; a configured-but-malformed
// key is a misconfiguration that must surface. keypairFromSolanaSecret throws
// the typed SolanaPrivateKeyInvalidError — let it propagate rather than
// swallowing it into a null that reads identically to "no wallet configured".
if (!raw) {
return null;
}
try {
return keypairFromSolanaSecret(raw);
} catch {
return null;
}
return keypairFromSolanaSecret(raw);
}

class LocalSolanaSigner implements SolanaSigner {
Expand Down
Loading