Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

197 lines
5.6 KiB
TypeScript

/**
* Delivers sensitive/approval requests to a Discord user as a DM, implementing
* `SensitiveRequestDeliveryAdapter` so the runtime's approval flow can target
* the `discord` source. Availability depends on the `DiscordService` being
* registered.
*/
import {
type DeliveryResult,
type DispatchSensitiveRequest,
type IAgentRuntime,
logger,
type SensitiveRequest,
type SensitiveRequestDeliveryAdapter,
} from "@elizaos/core";
import type { UserResolvable } from "discord.js";
import { DISCORD_SERVICE_NAME } from "./constants";
import type { DiscordService } from "./service";
// The Discord adapter receives the dispatch shape but reads policy-side
// fields (`delivery`, `callback`, `requesterEntityId`) when present. We
// type-narrow at the boundary via a permissive intersection.
type DiscordDispatchRequest = DispatchSensitiveRequest &
Partial<SensitiveRequest>;
const SENSITIVE_REQUEST_DISPATCH_REGISTRY_SERVICE_NAME =
"SensitiveRequestDispatchRegistry";
interface DiscordSendDmDeps {
getDiscordService(runtime: IAgentRuntime): DiscordService | null;
}
const defaultDeps: DiscordSendDmDeps = {
getDiscordService(runtime) {
const svc = runtime.getService?.(DISCORD_SERVICE_NAME) as
| DiscordService
| null
| undefined;
return svc ?? null;
},
};
function isCloudPaired(runtime: IAgentRuntime): boolean {
const apiKey = runtime.getSetting?.("ELIZA_CLOUD_API_KEY");
return typeof apiKey === "string" && apiKey.trim().length > 0;
}
function buildDmBody(
request: DiscordDispatchRequest,
runtime: IAgentRuntime,
): string {
const reason = request.delivery?.reason ?? "A sensitive value is required.";
const instruction =
request.delivery?.instruction ??
"Please open the Eliza app private chat to provide this value.";
if (request.kind === "secret" && isCloudPaired(runtime)) {
const link =
request.callback?.url ?? request.delivery?.linkBaseUrl ?? undefined;
if (link) {
return [
"A sensitive value is needed to continue.",
reason,
`Open this secure link to provide it: ${link}`,
`This link expires at ${request.expiresAt}.`,
].join("\n");
}
}
return [
"A sensitive value is needed to continue.",
reason,
instruction,
`This request expires at ${request.expiresAt}.`,
].join("\n");
}
async function deliverViaDiscordDm(
args: {
request: DispatchSensitiveRequest;
channelId?: string;
runtime: unknown;
},
deps: DiscordSendDmDeps,
): Promise<DeliveryResult> {
const runtime = args.runtime as IAgentRuntime;
const request = args.request as DiscordDispatchRequest;
const candidateDiscordUserId =
args.channelId ?? request.requesterEntityId ?? request.originUserId;
const discordUserId: string | null =
typeof candidateDiscordUserId === "string" ? candidateDiscordUserId : null;
if (!discordUserId) {
return {
delivered: false,
target: "dm",
error:
"No Discord user id available (need targetChannelId or originUserId)",
};
}
const discordService = deps.getDiscordService(runtime);
if (!discordService?.client) {
return {
delivered: false,
target: "dm",
error: "Discord service unavailable",
};
}
try {
const user = await discordService.client.users.fetch(
discordUserId as UserResolvable,
);
const dmChannel = await user.createDM();
await dmChannel.send({ content: buildDmBody(request, runtime) });
return {
delivered: true,
target: "dm",
channelId: dmChannel.id,
expiresAt: request.expiresAt,
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.warn(
{ src: "discord:sensitive-request-adapter", err: message },
"Failed to deliver sensitive request DM",
);
return { delivered: false, target: "dm", error: message };
}
}
export const discordDmSensitiveRequestAdapter: SensitiveRequestDeliveryAdapter =
{
target: "dm",
supportsChannel: (_channelId, runtime) => {
const svc = (runtime as IAgentRuntime | undefined)?.getService?.(
DISCORD_SERVICE_NAME,
) as DiscordService | null | undefined;
return Boolean(svc?.client);
},
deliver: (args) => deliverViaDiscordDm(args, defaultDeps),
};
/**
* Internal factory used by tests so the Discord client lookup can be mocked
* without standing up a real DiscordService instance.
*/
export function createDiscordDmSensitiveRequestAdapter(
deps: DiscordSendDmDeps,
): SensitiveRequestDeliveryAdapter {
return {
target: "dm",
supportsChannel: (_channelId, runtime) =>
Boolean(deps.getDiscordService(runtime as IAgentRuntime)?.client),
deliver: (args) => deliverViaDiscordDm(args, deps),
};
}
interface DispatchRegistryLike {
register: (adapter: SensitiveRequestDeliveryAdapter) => void;
}
/**
* Registers the Discord DM adapter into the runtime's
* SensitiveRequestDispatchRegistry, if one is available. Safe to call
* multiple times and from any plugin lifecycle hook; never throws.
*/
export function registerDiscordDmSensitiveRequestAdapter(
runtime: IAgentRuntime,
): void {
const tryRegister = (): boolean => {
const registry = runtime.getService?.(
SENSITIVE_REQUEST_DISPATCH_REGISTRY_SERVICE_NAME,
) as DispatchRegistryLike | null | undefined;
if (!registry || typeof registry.register !== "function") return false;
try {
registry.register(discordDmSensitiveRequestAdapter);
return true;
} catch (err) {
logger.warn(
{
src: "discord:sensitive-request-adapter",
err: err instanceof Error ? err.message : String(err),
},
"Failed to register Discord DM adapter with SensitiveRequestDispatchRegistry",
);
return true; // do not retry on a hard failure
}
};
if (tryRegister()) return;
// Registry may register slightly after plugin init; defer once.
setImmediate(() => {
tryRegister();
});
}