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

700 lines
19 KiB
TypeScript

/**
* Allowlist Management
*
* Functions for managing the exec approval allowlist.
* Handles loading, saving, and modifying allowlist entries.
*/
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { logger, resolveStateDir } from "@elizaos/core";
import type {
CommandResolution,
ExecAllowlistEntry,
ExecApprovalsAgent,
ExecApprovalsDefaults,
ExecApprovalsFile,
ExecApprovalsResolved,
ExecApprovalsSnapshot,
ExecAsk,
ExecSecurity,
} from "./types";
import { EXEC_APPROVAL_DEFAULTS } from "./types";
/** Default agent ID */
const DEFAULT_AGENT_ID = "default";
/**
* Expand home directory in path
*/
function expandHome(value: string): string {
if (!value) return value;
if (value === "~") return os.homedir();
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
return value;
}
/**
* Hash raw content for change detection
*/
function hashContent(raw: string | null): string {
return crypto
.createHash("sha256")
.update(raw ?? "")
.digest("hex");
}
/**
* Get the default approval file path
*/
export function getApprovalFilePath(): string {
return path.join(resolveStateDir(), "exec-approvals.json");
}
/**
* Get the default socket path
*/
export function getApprovalSocketPath(): string {
return path.join(resolveStateDir(), "exec-approvals.sock");
}
/**
* Ensure directory exists for a file path
*/
function ensureDir(filePath: string): void {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
}
/**
* Normalize allowlist pattern for comparison
*/
function normalizePattern(value: string | undefined): string | null {
const trimmed = value?.trim() ?? "";
return trimmed ? trimmed.toLowerCase() : null;
}
/**
* Ensure all allowlist entries have IDs
*/
function ensureAllowlistIds(
allowlist: ExecAllowlistEntry[] | undefined
): ExecAllowlistEntry[] | undefined {
if (!Array.isArray(allowlist) || allowlist.length === 0) {
return allowlist;
}
let changed = false;
const next = allowlist.map((entry) => {
if (entry.id) return entry;
changed = true;
return { ...entry, id: crypto.randomUUID() };
});
return changed ? next : allowlist;
}
/**
* Merge legacy agent configuration with current
*/
function mergeLegacyAgent(
current: ExecApprovalsAgent,
legacy: ExecApprovalsAgent
): ExecApprovalsAgent {
const allowlist: ExecAllowlistEntry[] = [];
const seen = new Set<string>();
const pushEntry = (entry: ExecAllowlistEntry) => {
const key = normalizePattern(entry.pattern);
if (!key || seen.has(key)) return;
seen.add(key);
allowlist.push(entry);
};
for (const entry of current.allowlist ?? []) pushEntry(entry);
for (const entry of legacy.allowlist ?? []) pushEntry(entry);
return {
security: current.security ?? legacy.security,
ask: current.ask ?? legacy.ask,
askFallback: current.askFallback ?? legacy.askFallback,
autoAllowSkills: current.autoAllowSkills ?? legacy.autoAllowSkills,
allowlist: allowlist.length > 0 ? allowlist : undefined,
};
}
/**
* Normalize approval configuration file
*/
export function normalizeApprovals(file: ExecApprovalsFile): ExecApprovalsFile {
const socketPath = file.socket?.path?.trim();
const token = file.socket?.token?.trim();
const agents = { ...file.agents };
// Handle legacy "default" agent
const legacyDefault = agents.default;
if (legacyDefault) {
const main = agents[DEFAULT_AGENT_ID];
agents[DEFAULT_AGENT_ID] = main ? mergeLegacyAgent(main, legacyDefault) : legacyDefault;
delete agents.default;
}
// Ensure all allowlist entries have IDs
for (const [key, agent] of Object.entries(agents)) {
const allowlist = ensureAllowlistIds(agent.allowlist);
if (allowlist !== agent.allowlist) {
agents[key] = { ...agent, allowlist };
}
}
return {
version: 1,
socket: {
path: socketPath && socketPath.length > 0 ? socketPath : undefined,
token: token && token.length > 0 ? token : undefined,
},
defaults: {
security: file.defaults?.security,
ask: file.defaults?.ask,
askFallback: file.defaults?.askFallback,
autoAllowSkills: file.defaults?.autoAllowSkills,
},
agents,
};
}
/**
* Generate a secure token
*/
function generateToken(): string {
return crypto.randomBytes(24).toString("base64url");
}
/**
* Read approval configuration snapshot
*/
export function readApprovalsSnapshot(): ExecApprovalsSnapshot {
const filePath = getApprovalFilePath();
if (!fs.existsSync(filePath)) {
const file = normalizeApprovals({ version: 1, agents: {} });
return {
path: filePath,
exists: false,
raw: null,
file,
hash: hashContent(null),
};
}
const raw = fs.readFileSync(filePath, "utf8");
let parsed: ExecApprovalsFile | null = null;
try {
parsed = JSON.parse(raw) as ExecApprovalsFile;
} catch (parseError) {
// error-policy:J3 untrusted on-disk config; a corrupt snapshot yields the
// explicit null so the caller falls back to the deny-all default below,
// never a fake-valid parse.
logger.warn(
{ src: "exec-approval", parseError, filePath },
"Failed to parse approval config snapshot - file may be corrupted"
);
parsed = null;
}
const file =
parsed?.version === 1
? normalizeApprovals(parsed)
: normalizeApprovals({ version: 1, agents: {} });
if (parsed && parsed.version !== 1) {
logger.warn(
{ src: "exec-approval", version: parsed.version, filePath },
"Approval config snapshot has unexpected version"
);
}
return {
path: filePath,
exists: true,
raw,
file,
hash: hashContent(raw),
};
}
/**
* Load approval configuration
*/
export function loadApprovals(): ExecApprovalsFile {
const filePath = getApprovalFilePath();
try {
if (!fs.existsSync(filePath)) {
logger.debug(
{ src: "exec-approval", filePath },
"Approval config file does not exist, using defaults"
);
return normalizeApprovals({ version: 1, agents: {} });
}
const raw = fs.readFileSync(filePath, "utf8");
let parsed: ExecApprovalsFile;
try {
parsed = JSON.parse(raw) as ExecApprovalsFile;
} catch (parseError) {
// error-policy:J3 untrusted on-disk config; a corrupt file resolves to
// the fail-closed deny-all default (empty agents), never a permissive
// fabricated config.
logger.error(
{ src: "exec-approval", parseError, filePath },
"Failed to parse approval config JSON - file may be corrupted. Using defaults."
);
return normalizeApprovals({ version: 1, agents: {} });
}
if (parsed.version !== 1) {
logger.warn(
{ src: "exec-approval", version: parsed.version, filePath },
"Approval config has unexpected version, using defaults"
);
return normalizeApprovals({ version: 1, agents: {} });
}
return normalizeApprovals(parsed);
} catch (error) {
// error-policy:J4 read failure (e.g. EACCES) degrades to the fail-closed
// deny-all default so the gate never opens on a load error; logged at error
// so the permissions problem is visible to the operator.
logger.error(
{ src: "exec-approval", error, filePath },
"Failed to load approval config - using defaults. This may indicate a permissions issue."
);
return normalizeApprovals({ version: 1, agents: {} });
}
}
/**
* Save approval configuration
* @throws Error if file cannot be written
*/
export function saveApprovals(file: ExecApprovalsFile): void {
const filePath = getApprovalFilePath();
try {
ensureDir(filePath);
fs.writeFileSync(filePath, `${JSON.stringify(file, null, 2)}\n`, {
mode: 0o600,
});
try {
fs.chmodSync(filePath, 0o600);
} catch {
// error-policy:J6 best-effort hardening; platforms without POSIX chmod
// (e.g. Windows) skip the mode tightening — the file is already written.
}
} catch (error) {
// error-policy:J2 boundary rethrow with `cause`; the write failure is
// re-raised (approvals must not silently fail to persist) with the original
// error preserved for diagnosis.
logger.error(
{ src: "exec-approval", error, filePath },
"Failed to save approval configuration"
);
throw new Error(`Failed to save approval configuration to ${filePath}`, {
cause: error,
});
}
}
/**
* Ensure approval configuration exists with socket/token
* @throws Error if configuration cannot be loaded or saved
*/
export function ensureApprovals(): ExecApprovalsFile {
const loaded = loadApprovals();
const next = normalizeApprovals(loaded);
const socketPath = next.socket?.path?.trim();
const token = next.socket?.token?.trim();
const updated: ExecApprovalsFile = {
...next,
socket: {
path: socketPath && socketPath.length > 0 ? socketPath : getApprovalSocketPath(),
token: token && token.length > 0 ? token : generateToken(),
},
};
// Try to save - if it fails, we can still return the in-memory config
// but the caller should handle the error appropriately
try {
saveApprovals(updated);
} catch (error) {
// error-policy:J2 warn for operator visibility, then rethrow the original
// error unchanged so the caller (resolveApprovals) owns the degrade
// decision rather than this helper silently returning a non-persisted config.
logger.warn(
{ src: "exec-approval", error },
"Failed to save approval config during ensureApprovals - " +
"returning in-memory config. Changes will not persist."
);
throw error;
}
return updated;
}
/**
* Normalize security value
*/
function normalizeSecurity(value: ExecSecurity | undefined, fallback: ExecSecurity): ExecSecurity {
if (value === "allowlist" || value === "full" || value === "deny") {
return value;
}
return fallback;
}
/**
* Normalize ask value
*/
function normalizeAsk(value: ExecAsk | undefined, fallback: ExecAsk): ExecAsk {
if (value === "always" || value === "off" || value === "on-miss") {
return value;
}
return fallback;
}
/**
* Resolve approval configuration for an agent
* @throws Error if configuration cannot be loaded or saved
*/
export function resolveApprovals(
agentId?: string,
overrides?: Partial<ExecApprovalsDefaults>
): ExecApprovalsResolved {
// Try to ensure approvals exist (may fail if can't write to disk)
// If it fails, we load what we can and proceed with that
let file: ExecApprovalsFile;
try {
file = ensureApprovals();
} catch (error) {
// error-policy:J4 write path unavailable (read-only FS / EACCES) → degrade
// to the read-only loaded config so the gate still resolves; logged so the
// non-persistent state is visible. loadApprovals itself fails closed.
logger.warn(
{ src: "exec-approval", error },
"Could not ensure approval config exists - using read-only config"
);
file = loadApprovals();
}
return resolveApprovalsFromFile({
file,
agentId,
overrides,
path: getApprovalFilePath(),
socketPath: expandHome(file.socket?.path ?? getApprovalSocketPath()),
token: file.socket?.token ?? "",
});
}
/**
* Resolve approval configuration from a loaded file
*/
export function resolveApprovalsFromFile(params: {
file: ExecApprovalsFile;
agentId?: string;
overrides?: Partial<ExecApprovalsDefaults>;
path?: string;
socketPath?: string;
token?: string;
}): ExecApprovalsResolved {
const file = normalizeApprovals(params.file);
const defaults = file.defaults ?? {};
const agentKey = params.agentId ?? DEFAULT_AGENT_ID;
const agent = file.agents?.[agentKey] ?? {};
const wildcard = file.agents?.["*"] ?? {};
const fallbackSecurity = params.overrides?.security ?? EXEC_APPROVAL_DEFAULTS.security;
const fallbackAsk = params.overrides?.ask ?? EXEC_APPROVAL_DEFAULTS.ask;
const fallbackAskFallback = params.overrides?.askFallback ?? EXEC_APPROVAL_DEFAULTS.askFallback;
const fallbackAutoAllowSkills =
params.overrides?.autoAllowSkills ?? EXEC_APPROVAL_DEFAULTS.autoAllowSkills;
const resolvedDefaults: Required<ExecApprovalsDefaults> = {
security: normalizeSecurity(defaults.security, fallbackSecurity),
ask: normalizeAsk(defaults.ask, fallbackAsk),
askFallback: normalizeSecurity(
defaults.askFallback ?? fallbackAskFallback,
fallbackAskFallback
),
autoAllowSkills: Boolean(defaults.autoAllowSkills ?? fallbackAutoAllowSkills),
};
const resolvedAgent: Required<ExecApprovalsDefaults> = {
security: normalizeSecurity(
agent.security ?? wildcard.security ?? resolvedDefaults.security,
resolvedDefaults.security
),
ask: normalizeAsk(agent.ask ?? wildcard.ask ?? resolvedDefaults.ask, resolvedDefaults.ask),
askFallback: normalizeSecurity(
agent.askFallback ?? wildcard.askFallback ?? resolvedDefaults.askFallback,
resolvedDefaults.askFallback
),
autoAllowSkills: Boolean(
agent.autoAllowSkills ?? wildcard.autoAllowSkills ?? resolvedDefaults.autoAllowSkills
),
};
const allowlist = [
...(Array.isArray(wildcard.allowlist) ? wildcard.allowlist : []),
...(Array.isArray(agent.allowlist) ? agent.allowlist : []),
];
return {
path: params.path ?? getApprovalFilePath(),
socketPath: expandHome(params.socketPath ?? file.socket?.path ?? getApprovalSocketPath()),
token: params.token ?? file.socket?.token ?? "",
defaults: resolvedDefaults,
agent: resolvedAgent,
allowlist,
file,
};
}
/**
* Match command against allowlist
*/
export function matchAllowlist(
entries: ExecAllowlistEntry[],
resolution: CommandResolution | null
): ExecAllowlistEntry | null {
if (!entries.length || !resolution?.resolvedPath) {
return null;
}
const resolvedPath = resolution.resolvedPath;
for (const entry of entries) {
const pattern = entry.pattern.trim();
if (!pattern) continue;
const hasPath = pattern.includes("/") || pattern.includes("\\") || pattern.includes("~");
if (!hasPath) continue;
if (matchesPattern(pattern, resolvedPath)) {
return entry;
}
}
return null;
}
/**
* Check if pattern matches target
*/
function matchesPattern(pattern: string, target: string): boolean {
const trimmed = pattern.trim();
if (!trimmed) return false;
const expanded = trimmed.startsWith("~") ? expandHome(trimmed) : trimmed;
const hasWildcard = /[*?]/.test(expanded);
let normalizedPattern = expanded;
let normalizedTarget = target;
if (process.platform === "win32" && !hasWildcard) {
normalizedPattern = tryRealpath(expanded) ?? expanded;
normalizedTarget = tryRealpath(target) ?? target;
}
normalizedPattern = normalizeMatchTarget(normalizedPattern);
normalizedTarget = normalizeMatchTarget(normalizedTarget);
const regex = globToRegExp(normalizedPattern);
return regex.test(normalizedTarget);
}
/**
* Normalize path for matching
*/
function normalizeMatchTarget(value: string): string {
if (process.platform === "win32") {
const stripped = value.replace(/^\\\\[?.]\\/, "");
return stripped.replace(/\\/g, "/").toLowerCase();
}
return value.replace(/\\\\/g, "/").toLowerCase();
}
/**
* Try to get realpath (resolve symlinks)
*/
function tryRealpath(value: string): string | null {
try {
return fs.realpathSync(value);
} catch {
// error-policy:J3 realpath probe; an unresolvable path (absent, broken
// symlink) yields null so the caller keeps the original value.
return null;
}
}
/**
* Convert glob pattern to RegExp
*/
function globToRegExp(pattern: string): RegExp {
let regex = "^";
let i = 0;
while (i < pattern.length) {
const ch = pattern[i];
if (ch === "*") {
const next = pattern[i + 1];
if (next === "*") {
regex += ".*";
i += 2;
continue;
}
regex += "[^/]*";
i += 1;
continue;
}
if (ch === "?") {
regex += ".";
i += 1;
continue;
}
regex += ch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
i += 1;
}
regex += "$";
return new RegExp(regex, "i");
}
/**
* Record allowlist usage
* @returns true if successful, false if save failed
*/
export function recordAllowlistUse(
approvals: ExecApprovalsFile,
agentId: string | undefined,
entry: ExecAllowlistEntry,
command: string,
resolvedPath?: string
): boolean {
const target = agentId ?? DEFAULT_AGENT_ID;
const agents = approvals.agents ?? {};
const existing = agents[target] ?? {};
const allowlist = Array.isArray(existing.allowlist) ? existing.allowlist : [];
const nextAllowlist = allowlist.map((item) =>
item.pattern === entry.pattern
? {
...item,
id: item.id ?? crypto.randomUUID(),
lastUsedAt: Date.now(),
lastUsedCommand: command,
lastResolvedPath: resolvedPath,
}
: item
);
agents[target] = { ...existing, allowlist: nextAllowlist };
approvals.agents = agents;
try {
saveApprovals(approvals);
return true;
} catch (error) {
// error-policy:J6 best-effort usage bookkeeping (lastUsedAt); failing to
// persist a usage timestamp must not block the already-granted command, so
// it returns false (not-recorded) after a warn.
logger.warn(
{ src: "exec-approval", error, pattern: entry.pattern },
"Failed to record allowlist usage - continuing without update"
);
return false;
}
}
/**
* Add a new allowlist entry
* @returns true if entry was added, false if already exists or save failed
*/
export function addAllowlistEntry(
approvals: ExecApprovalsFile,
agentId: string | undefined,
pattern: string
): boolean {
const target = agentId ?? DEFAULT_AGENT_ID;
const agents = approvals.agents ?? {};
const existing = agents[target] ?? {};
const allowlist = Array.isArray(existing.allowlist) ? existing.allowlist : [];
const trimmed = pattern.trim();
if (!trimmed) {
logger.warn({ src: "exec-approval" }, "Attempted to add empty pattern to allowlist");
return false;
}
if (allowlist.some((entry) => entry.pattern === trimmed)) {
logger.debug({ src: "exec-approval", pattern: trimmed }, "Pattern already in allowlist");
return false;
}
allowlist.push({
id: crypto.randomUUID(),
pattern: trimmed,
lastUsedAt: Date.now(),
});
agents[target] = { ...existing, allowlist };
approvals.agents = agents;
try {
saveApprovals(approvals);
logger.info(
{ src: "exec-approval", pattern: trimmed, agentId: target },
"Added pattern to allowlist"
);
return true;
} catch (error) {
// error-policy:J1 boundary — the save failure is reported to the caller as
// `false` (entry not persisted) and logged at error; callers must treat
// false as "not added", never as success.
logger.error(
{ src: "exec-approval", error, pattern: trimmed },
"Failed to save allowlist after adding entry"
);
return false;
}
}
/**
* Get minimum security level
*/
export function minSecurity(a: ExecSecurity, b: ExecSecurity): ExecSecurity {
const order: Record<ExecSecurity, number> = {
deny: 0,
allowlist: 1,
full: 2,
};
return order[a] <= order[b] ? a : b;
}
/**
* Get maximum ask level
*/
export function maxAsk(a: ExecAsk, b: ExecAsk): ExecAsk {
const order: Record<ExecAsk, number> = { off: 0, "on-miss": 1, always: 2 };
return order[a] >= order[b] ? a : b;
}