mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🎚️ feat: Configure Agent Event Runtime in YAML (#15128)
This commit is contained in:
parent
dd146ff74d
commit
8969ee4b18
15 changed files with 233 additions and 65 deletions
28
.env.example
28
.env.example
|
|
@ -724,15 +724,6 @@ LIMIT_MESSAGE_USER=false
|
|||
MESSAGE_USER_MAX=40
|
||||
MESSAGE_USER_WINDOW=1
|
||||
|
||||
# Authenticated agent-event admission uses a separate API-key bucket because
|
||||
# delivery execution later consumes the normal message-user limit.
|
||||
AGENT_EVENT_USER_MAX=40
|
||||
AGENT_EVENT_USER_WINDOW=1
|
||||
|
||||
# Enable only after every API replica runs a release that understands
|
||||
# source-bound child actor continuations.
|
||||
ENABLE_AGENT_EVENT_CHILD_TURNS=false
|
||||
|
||||
ILLEGAL_MODEL_REQ_SCORE=5
|
||||
|
||||
#========================#
|
||||
|
|
@ -1221,25 +1212,6 @@ OPENWEATHER_API_KEY=
|
|||
# or
|
||||
# COHERE_API_KEY=your_cohere_api_key
|
||||
|
||||
#===========================#
|
||||
# Agent Trigger Delivery #
|
||||
#===========================#
|
||||
|
||||
# Base URL used by trusted in-process event producers to dispatch agent fires, continuations,
|
||||
# and steers.
|
||||
# Defaults to this process's bound listener. Set only when internal trigger admission must
|
||||
# traverse another trusted HTTP origin, such as a TLS front door.
|
||||
# AGENT_TRIGGERS_SELF_URL=http://127.0.0.1:3080
|
||||
|
||||
# Automatically continue a saved parent agent after a detached subagent settles.
|
||||
# Rolling-deploy safety: deploy support with this disabled first, wait until every API
|
||||
# replica is upgraded, then enable it in a subsequent rollout.
|
||||
# ENABLE_SUBAGENT_COMPLETION_WAKEUPS=false
|
||||
|
||||
# Trusted event adapters enqueue through the shared durable trigger service. Mongo-backed
|
||||
# leases make its workers safe across replicas; successful delivery records expire after
|
||||
# 90 days, while dead letters remain available for explicit operator requeue.
|
||||
|
||||
#======================#
|
||||
# MCP Configuration #
|
||||
#======================#
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const {
|
|||
updateInterfacePermissions,
|
||||
configureMessageFilterRegexValidator,
|
||||
configureFileConfigRegexEngine,
|
||||
configureAgentEventRuntime,
|
||||
createScheduleWriteGate,
|
||||
waitForKeyvRedisClient,
|
||||
} = require('@librechat/api');
|
||||
|
|
@ -175,6 +176,7 @@ const startServer = async () => {
|
|||
logger.error('[sweepOrphanedPreviews] Background sweep failed:', err);
|
||||
});
|
||||
const appConfig = await getAppConfig({ baseOnly: true });
|
||||
configureAgentEventRuntime(appConfig?.endpoints?.agents?.eventDriven);
|
||||
initializeFileStorage(appConfig);
|
||||
const projectRoot = path.resolve(__dirname, '../..');
|
||||
// Plugin hooks execute only when the operator opts in via DEPLOYMENT_PLUGIN_HOOKS;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ const {
|
|||
MESSAGE_IP_WINDOW = 1,
|
||||
MESSAGE_USER_MAX = 40,
|
||||
MESSAGE_USER_WINDOW = 1,
|
||||
AGENT_EVENT_USER_MAX = 40,
|
||||
AGENT_EVENT_USER_WINDOW = 1,
|
||||
MESSAGE_VIOLATION_SCORE: score,
|
||||
} = process.env;
|
||||
|
||||
|
|
@ -22,10 +20,6 @@ const userWindowMs = MESSAGE_USER_WINDOW * 60 * 1000;
|
|||
const userMax = MESSAGE_USER_MAX;
|
||||
const userWindowInMinutes = userWindowMs / 60000;
|
||||
|
||||
const agentEventUserWindowMs = AGENT_EVENT_USER_WINDOW * 60 * 1000;
|
||||
const agentEventUserMax = AGENT_EVENT_USER_MAX;
|
||||
const agentEventUserWindowInMinutes = agentEventUserWindowMs / 60000;
|
||||
|
||||
/**
|
||||
* Creates either an IP/User message request rate limiter for excessive requests
|
||||
* that properly logs and denies the violation.
|
||||
|
|
@ -85,23 +79,31 @@ const messageUserLimiter = rateLimit(userLimiterOptions);
|
|||
* consumes the normal message-user bucket when it executes the delivery, so
|
||||
* sharing that limiter here would charge every event twice.
|
||||
*/
|
||||
const agentEventUserLimiter = rateLimit({
|
||||
windowMs: agentEventUserWindowMs,
|
||||
max: agentEventUserMax,
|
||||
handler: async (req, res) => {
|
||||
const type = ViolationTypes.MESSAGE_LIMIT;
|
||||
const errorMessage = {
|
||||
type,
|
||||
max: agentEventUserMax,
|
||||
limiter: 'agent_event_principal',
|
||||
windowInMinutes: agentEventUserWindowInMinutes,
|
||||
};
|
||||
await logViolation(req, res, type, errorMessage, score);
|
||||
return await denyRequest(req, res, errorMessage);
|
||||
},
|
||||
keyGenerator: (req) => String(req.apiKeyId ?? req.user?.id),
|
||||
store: limiterCache('agent_event_user_limiter'),
|
||||
});
|
||||
let configuredAgentEventUserLimiter;
|
||||
const agentEventUserLimiter = (req, res, next) => {
|
||||
if (configuredAgentEventUserLimiter == null) {
|
||||
const max = Number(process.env.AGENT_EVENT_USER_MAX ?? 40);
|
||||
const windowInMinutes = Number(process.env.AGENT_EVENT_USER_WINDOW ?? 1);
|
||||
configuredAgentEventUserLimiter = rateLimit({
|
||||
windowMs: windowInMinutes * 60 * 1000,
|
||||
max,
|
||||
handler: async (limitedReq, limitedRes) => {
|
||||
const type = ViolationTypes.MESSAGE_LIMIT;
|
||||
const errorMessage = {
|
||||
type,
|
||||
max,
|
||||
limiter: 'agent_event_principal',
|
||||
windowInMinutes,
|
||||
};
|
||||
await logViolation(limitedReq, limitedRes, type, errorMessage, score);
|
||||
return await denyRequest(limitedReq, limitedRes, errorMessage);
|
||||
},
|
||||
keyGenerator: (limitedReq) => String(limitedReq.apiKeyId ?? limitedReq.user?.id),
|
||||
store: limiterCache('agent_event_user_limiter'),
|
||||
});
|
||||
}
|
||||
return configuredAgentEventUserLimiter(req, res, next);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
agentEventUserLimiter,
|
||||
|
|
|
|||
26
api/server/middleware/limiters/messageLimiters.spec.js
Normal file
26
api/server/middleware/limiters/messageLimiters.spec.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const mockLimiter = jest.fn((_req, _res, next) => next());
|
||||
const mockRateLimit = jest.fn(() => mockLimiter);
|
||||
|
||||
jest.mock('express-rate-limit', () => mockRateLimit);
|
||||
jest.mock('@librechat/api', () => ({
|
||||
limiterCache: jest.fn(() => ({})),
|
||||
removePorts: jest.fn(),
|
||||
}));
|
||||
jest.mock('~/server/middleware/denyRequest', () => jest.fn());
|
||||
jest.mock('~/cache', () => ({ logViolation: jest.fn() }));
|
||||
|
||||
describe('agent event rate limiter', () => {
|
||||
it('reads YAML-projected limits lazily after startup configuration', () => {
|
||||
process.env.AGENT_EVENT_USER_MAX = '80';
|
||||
process.env.AGENT_EVENT_USER_WINDOW = '2';
|
||||
const { agentEventUserLimiter } = require('./messageLimiters');
|
||||
const next = jest.fn();
|
||||
|
||||
agentEventUserLimiter({ apiKeyId: 'key-1' }, {}, next);
|
||||
|
||||
expect(mockRateLimit).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ max: 80, windowMs: 120_000 }),
|
||||
);
|
||||
expect(mockLimiter).toHaveBeenCalledWith({ apiKeyId: 'key-1' }, {}, next);
|
||||
});
|
||||
});
|
||||
|
|
@ -14,12 +14,10 @@ const {
|
|||
const db = require('~/models');
|
||||
const { enqueueAgentTrigger } = require('../../Agents/triggers');
|
||||
|
||||
/** Keep producers off for the first rollout so older trigger workers cannot
|
||||
* permanently reject the new `continue` envelope. Enable only after every API
|
||||
* replica runs a release that understands completion wakeups. */
|
||||
const completionWakeupsEnabled = isEnabled(process.env.ENABLE_SUBAGENT_COMPLETION_WAKEUPS);
|
||||
const GENERATION_DRAIN_TIMEOUT_MS = 45_000;
|
||||
const GENERATION_DRAIN_POLL_MS = 100;
|
||||
const completionWakeupHandler = createSubagentCompletionWakeupHandler(enqueueAgentTrigger);
|
||||
const completionWakeupsEnabled = () => isEnabled(process.env.ENABLE_SUBAGENT_COMPLETION_WAKEUPS);
|
||||
|
||||
async function cancelUnroutedGeneration({ userId, tenantId, taskId }) {
|
||||
let job = await GenerationJobManager.getJob(taskId);
|
||||
|
|
@ -78,9 +76,12 @@ const subagentThreadTaskStore = createSubagentThreadTaskStore(
|
|||
renewOwnerAdmission: db.renewSubagentAdmission,
|
||||
releaseOwnerAdmission: db.releaseSubagentAdmission,
|
||||
cancelUnroutedTask: cancelUnroutedGeneration,
|
||||
...(completionWakeupsEnabled && {
|
||||
onTaskPrepared: createSubagentCompletionWakeupHandler(enqueueAgentTrigger),
|
||||
}),
|
||||
onTaskPrepared: (registration) => {
|
||||
if (!completionWakeupsEnabled()) {
|
||||
return;
|
||||
}
|
||||
return completionWakeupHandler(registration);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -138,5 +139,8 @@ async function configureSubagentTaskRouting() {
|
|||
}
|
||||
|
||||
module.exports = subagentThreadTaskStore;
|
||||
module.exports.completionWakeupsEnabled = completionWakeupsEnabled;
|
||||
Object.defineProperty(module.exports, 'completionWakeupsEnabled', {
|
||||
enumerable: true,
|
||||
get: completionWakeupsEnabled,
|
||||
});
|
||||
module.exports.configureSubagentTaskRouting = configureSubagentTaskRouting;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const mockTaskStore = {
|
|||
destroyTaskControlTransport: jest.fn().mockResolvedValue(undefined),
|
||||
destroyActivityStream: jest.fn(),
|
||||
};
|
||||
const mockCompletionWakeupHandler = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
jest.mock('@librechat/api', () => ({
|
||||
cacheConfig: { USE_REDIS: true, REDIS_KEY_PREFIX: 'test:' },
|
||||
|
|
@ -13,7 +14,7 @@ jest.mock('@librechat/api', () => ({
|
|||
registerShutdownTask: jest.fn(),
|
||||
duplicateIoRedisClient: jest.fn(),
|
||||
createSubagentThreadTaskStore: jest.fn(() => mockTaskStore),
|
||||
createSubagentCompletionWakeupHandler: jest.fn(),
|
||||
createSubagentCompletionWakeupHandler: jest.fn(() => mockCompletionWakeupHandler),
|
||||
RedisSubagentTaskControlTransport: jest.fn(),
|
||||
RedisEventTransport: jest.fn(),
|
||||
SubagentActivityStream: jest.fn(),
|
||||
|
|
@ -44,13 +45,35 @@ jest.mock('../../Agents/triggers', () => ({
|
|||
enqueueAgentTrigger: jest.fn(),
|
||||
}));
|
||||
|
||||
const { ioredisClient, registerShutdownTask, duplicateIoRedisClient } = require('@librechat/api');
|
||||
const { configureSubagentTaskRouting } = require('./subagentThreadStore');
|
||||
const {
|
||||
ioredisClient,
|
||||
isEnabled,
|
||||
registerShutdownTask,
|
||||
duplicateIoRedisClient,
|
||||
createSubagentThreadTaskStore,
|
||||
} = require('@librechat/api');
|
||||
const subagentThreadTaskStore = require('./subagentThreadStore');
|
||||
const { configureSubagentTaskRouting } = subagentThreadTaskStore;
|
||||
const taskStoreOptions = createSubagentThreadTaskStore.mock.calls[0][1];
|
||||
const activityPrepareRegistration = registerShutdownTask.mock.calls.find(
|
||||
([name]) => name === 'subagent activity streams prepare',
|
||||
);
|
||||
|
||||
describe('subagent thread Redis lifecycle', () => {
|
||||
it('reads completion wakeup rollout state at task preparation time', async () => {
|
||||
isEnabled.mockReturnValueOnce(false);
|
||||
|
||||
await taskStoreOptions.onTaskPrepared({ taskId: 'disabled' });
|
||||
expect(mockCompletionWakeupHandler).not.toHaveBeenCalled();
|
||||
isEnabled.mockReturnValueOnce(true);
|
||||
await taskStoreOptions.onTaskPrepared({ taskId: 'enabled' });
|
||||
expect(mockCompletionWakeupHandler).toHaveBeenCalledWith({ taskId: 'enabled' });
|
||||
isEnabled.mockReturnValueOnce(true);
|
||||
expect(subagentThreadTaskStore.completionWakeupsEnabled).toBe(true);
|
||||
isEnabled.mockReturnValueOnce(false);
|
||||
expect(subagentThreadTaskStore.completionWakeupsEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('closes activity SSE before drain and disconnects its subscriber after drain', async () => {
|
||||
const taskSubscriber = { disconnect: jest.fn() };
|
||||
const activitySubscriber = { disconnect: jest.fn() };
|
||||
|
|
|
|||
|
|
@ -361,6 +361,10 @@ registration:
|
|||
# - '127.0.0.1:8080'
|
||||
|
||||
# rateLimits:
|
||||
# # Authenticated agent-event admission has its own API-key-principal bucket.
|
||||
# agentEvents:
|
||||
# userMax: 40
|
||||
# userWindowInMinutes: 1
|
||||
# fileUploads:
|
||||
# ipMax: 100
|
||||
# ipWindowInMinutes: 60 # Rate limit window for file uploads per IP
|
||||
|
|
@ -559,6 +563,14 @@ endpoints:
|
|||
# # (optional) Limit the workspace scopes users may select. Omit to allow all three.
|
||||
# statefulCodeSessions:
|
||||
# allowedEnvironments: ["user", "agent-user", "conversation"]
|
||||
# # (optional) Process-wide event-driven agent rollout controls. Deploy support to
|
||||
# # every API replica before enabling either producer during a subsequent rollout.
|
||||
# eventDriven:
|
||||
# childTurns: false
|
||||
# completionWakeups: false
|
||||
# # Optional trusted origin for internal trigger delivery. By default, LibreChat
|
||||
# # uses its own bound listener; override only for an internal TLS/front-door route.
|
||||
# # selfUrl: 'https://librechat.internal'
|
||||
# # "run_in_background" makes Code Interpreter tools eligible by default and enables per-tool MCP opt-in.
|
||||
# # "tool_intents" enables live model-written labels for native tools and opted-in MCP tools.
|
||||
# # (optional) Require user approval before matching tool calls. Disabled by default.
|
||||
|
|
|
|||
|
|
@ -114,9 +114,13 @@ Register a direct child agent once under the same Remote Agents API key that wil
|
|||
The parent must be an ordinary agent conversation, and the target must be enabled in that parent
|
||||
agent's direct `subagents.agent_ids` list (or be an allowed self-spawn). The reserved child
|
||||
conversation is hidden from conversation lists and remains read-only to human chat routes.
|
||||
`ENABLE_AGENT_EVENT_CHILD_TURNS` defaults to false; enable it only after every API replica runs a
|
||||
release that understands bound child continuations, otherwise an older worker could permanently
|
||||
reject a new envelope during a rolling deployment.
|
||||
`endpoints.agents.eventDriven.childTurns` defaults to false; enable it only after every API replica
|
||||
runs a release that understands bound child continuations, otherwise an older worker could
|
||||
permanently reject a new envelope during a rolling deployment. The legacy
|
||||
`ENABLE_AGENT_EVENT_CHILD_TURNS` environment variable remains a compatibility fallback.
|
||||
`AGENT_TRIGGERS_SELF_URL` likewise remains a compatibility fallback for
|
||||
`endpoints.agents.eventDriven.selfUrl`; most deployments should omit both and use the bound
|
||||
listener.
|
||||
|
||||
```http
|
||||
POST /api/agents/v1/events/bindings
|
||||
|
|
|
|||
40
packages/api/src/app/agents.spec.ts
Normal file
40
packages/api/src/app/agents.spec.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { configureAgentEventRuntime } from './agents';
|
||||
|
||||
describe('configureAgentEventRuntime', () => {
|
||||
const originalEnvironment = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnvironment };
|
||||
delete process.env.ENABLE_AGENT_EVENT_CHILD_TURNS;
|
||||
delete process.env.ENABLE_SUBAGENT_COMPLETION_WAKEUPS;
|
||||
delete process.env.AGENT_TRIGGERS_SELF_URL;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
process.env = originalEnvironment;
|
||||
});
|
||||
|
||||
it('projects explicit base-config rollout flags into the legacy runtime seam', () => {
|
||||
configureAgentEventRuntime({
|
||||
childTurns: true,
|
||||
completionWakeups: false,
|
||||
selfUrl: 'https://triggers.internal',
|
||||
});
|
||||
|
||||
expect(process.env.ENABLE_AGENT_EVENT_CHILD_TURNS).toBe('true');
|
||||
expect(process.env.ENABLE_SUBAGENT_COMPLETION_WAKEUPS).toBe('false');
|
||||
expect(process.env.AGENT_TRIGGERS_SELF_URL).toBe('https://triggers.internal');
|
||||
});
|
||||
|
||||
it('preserves environment fallbacks when the YAML fields are omitted', () => {
|
||||
process.env.ENABLE_AGENT_EVENT_CHILD_TURNS = 'true';
|
||||
process.env.ENABLE_SUBAGENT_COMPLETION_WAKEUPS = 'true';
|
||||
process.env.AGENT_TRIGGERS_SELF_URL = 'https://legacy.internal';
|
||||
|
||||
configureAgentEventRuntime(undefined);
|
||||
|
||||
expect(process.env.ENABLE_AGENT_EVENT_CHILD_TURNS).toBe('true');
|
||||
expect(process.env.ENABLE_SUBAGENT_COMPLETION_WAKEUPS).toBe('true');
|
||||
expect(process.env.AGENT_TRIGGERS_SELF_URL).toBe('https://legacy.internal');
|
||||
});
|
||||
});
|
||||
18
packages/api/src/app/agents.ts
Normal file
18
packages/api/src/app/agents.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { TAgentsEndpoint } from 'librechat-data-provider';
|
||||
|
||||
type AgentEventRuntimeConfig = NonNullable<TAgentsEndpoint['eventDriven']>;
|
||||
|
||||
const setBooleanEnvironmentFallback = (name: string, value?: boolean): void => {
|
||||
if (value != null) {
|
||||
process.env[name] = String(value);
|
||||
}
|
||||
};
|
||||
|
||||
/** Applies base-config rollout flags before the HTTP listener accepts agent events. */
|
||||
export const configureAgentEventRuntime = (config?: AgentEventRuntimeConfig): void => {
|
||||
setBooleanEnvironmentFallback('ENABLE_AGENT_EVENT_CHILD_TURNS', config?.childTurns);
|
||||
setBooleanEnvironmentFallback('ENABLE_SUBAGENT_COMPLETION_WAKEUPS', config?.completionWakeups);
|
||||
if (config?.selfUrl != null) {
|
||||
process.env.AGENT_TRIGGERS_SELF_URL = config.selfUrl;
|
||||
}
|
||||
};
|
||||
|
|
@ -355,4 +355,16 @@ describe('handleRateLimits', () => {
|
|||
expect(process.env.STT_USER_MAX).toEqual('30');
|
||||
expect(process.env.STT_USER_WINDOW).toEqual('20');
|
||||
});
|
||||
|
||||
it('should set authenticated agent-event admission limits', () => {
|
||||
handleRateLimits({
|
||||
agentEvents: {
|
||||
userMax: 80,
|
||||
userWindowInMinutes: 2,
|
||||
},
|
||||
});
|
||||
|
||||
expect(process.env.AGENT_EVENT_USER_MAX).toEqual('80');
|
||||
expect(process.env.AGENT_EVENT_USER_WINDOW).toEqual('2');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,5 +8,6 @@ export * from './resolve';
|
|||
export * from './shutdown';
|
||||
export * from './server';
|
||||
export * from './origin';
|
||||
export * from './agents';
|
||||
export { resolveBuildInfo } from './build';
|
||||
export type { BuildInfo } from './build';
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const handleRateLimits = (rateLimits?: TCustomConfig['rateLimits']): void
|
|||
}
|
||||
|
||||
const rateLimitKeys = {
|
||||
agentEvents: 'AGENT_EVENT',
|
||||
fileUploads: RateLimitPrefix.FILE_UPLOAD,
|
||||
conversationsImport: RateLimitPrefix.IMPORT,
|
||||
tts: RateLimitPrefix.TTS,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,40 @@ describe('bedrockEndpointSchema', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('agent event runtime config', () => {
|
||||
it('accepts rollout flags and agent-event admission limits', () => {
|
||||
const result = configSchema.safeParse({
|
||||
version: '1.0',
|
||||
endpoints: {
|
||||
agents: {
|
||||
eventDriven: {
|
||||
childTurns: true,
|
||||
completionWakeups: false,
|
||||
selfUrl: 'https://triggers.internal',
|
||||
},
|
||||
},
|
||||
},
|
||||
rateLimits: {
|
||||
agentEvents: { userMax: 80, userWindowInMinutes: 2 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
expect(result.data.endpoints?.agents?.eventDriven).toEqual({
|
||||
childTurns: true,
|
||||
completionWakeups: false,
|
||||
selfUrl: 'https://triggers.internal',
|
||||
});
|
||||
expect(result.data.rateLimits?.agentEvents).toEqual({
|
||||
userMax: 80,
|
||||
userWindowInMinutes: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('speechTab schema', () => {
|
||||
it.each(['browser', 'external', 'openai', 'azureOpenAI'])(
|
||||
'accepts the speech-to-text engine "%s"',
|
||||
|
|
|
|||
|
|
@ -1039,6 +1039,17 @@ export const agentsEndpointSchema = baseEndpointSchema
|
|||
allowedEnvironments: z.array(z.enum(STATEFUL_CODE_ENVIRONMENTS)).min(1),
|
||||
})
|
||||
.optional(),
|
||||
/** Process-wide event-driven agent rollout controls. Configure these from the base
|
||||
* deployment config only so every API replica exposes the same wire capabilities. */
|
||||
eventDriven: z
|
||||
.object({
|
||||
childTurns: z.boolean().optional(),
|
||||
completionWakeups: z.boolean().optional(),
|
||||
/** Optional trusted origin for in-process trigger delivery. The bound
|
||||
* listener remains the default and is safer for most deployments. */
|
||||
selfUrl: z.string().url().optional(),
|
||||
})
|
||||
.optional(),
|
||||
skills: z
|
||||
.object({
|
||||
maxCatalogSkills: z.number().int().min(1).max(100).optional(),
|
||||
|
|
@ -1394,6 +1405,12 @@ export enum RateLimitPrefix {
|
|||
}
|
||||
|
||||
export const rateLimitSchema = z.object({
|
||||
agentEvents: z
|
||||
.object({
|
||||
userMax: z.number().int().positive().optional(),
|
||||
userWindowInMinutes: z.number().positive().optional(),
|
||||
})
|
||||
.optional(),
|
||||
fileUploads: z
|
||||
.object({
|
||||
ipMax: z.number().optional(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue