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
197 lines
5.6 KiB
TypeScript
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();
|
|
});
|
|
}
|