create-safest-tools 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/create-safest-tools.mjs +12 -0
- package/package.json +34 -0
- package/src/cli.mjs +128 -0
- package/src/config.mjs +151 -0
- package/src/scaffold.mjs +30 -0
- package/template/LICENSE +201 -0
- package/template/README.md +35 -0
- package/template/console/README.md +28 -0
- package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
- package/template/console/analytics/controller.ts +133 -0
- package/template/console/analytics/events.ts +33 -0
- package/template/console/analytics/store.ts +33 -0
- package/template/console/analytics/types.ts +38 -0
- package/template/console/appeals/AppealWorkspace.tsx +73 -0
- package/template/console/appeals/controller.ts +125 -0
- package/template/console/appeals/events.ts +33 -0
- package/template/console/appeals/store.ts +35 -0
- package/template/console/appeals/types.ts +37 -0
- package/template/console/assistant/AssistantWorkspace.tsx +133 -0
- package/template/console/assistant/types.ts +34 -0
- package/template/console/auth/AccountJourney.tsx +69 -0
- package/template/console/auth/InvitationAcceptance.tsx +145 -0
- package/template/console/auth/OAuthButtons.tsx +78 -0
- package/template/console/auth/OperatorLogin.tsx +110 -0
- package/template/console/auth/PasswordRecovery.tsx +132 -0
- package/template/console/auth/account-route.ts +15 -0
- package/template/console/auth/browser-navigation.ts +7 -0
- package/template/console/auth/events.ts +15 -0
- package/template/console/command/CommandCentre.tsx +119 -0
- package/template/console/command/controller.ts +267 -0
- package/template/console/command/events.ts +19 -0
- package/template/console/command/store.ts +36 -0
- package/template/console/command/types.ts +61 -0
- package/template/console/components/AnalystIdentity.tsx +25 -0
- package/template/console/components/PageHeader.tsx +27 -0
- package/template/console/configuration/ConfigurationDialog.tsx +200 -0
- package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
- package/template/console/configuration/FormBuilder.tsx +143 -0
- package/template/console/configuration/api.ts +40 -0
- package/template/console/configuration/events.ts +14 -0
- package/template/console/configuration/types.ts +79 -0
- package/template/console/lib/format.ts +49 -0
- package/template/console/lib/http.ts +96 -0
- package/template/console/main.tsx +225 -0
- package/template/console/operations/OperationsWorkspace.tsx +126 -0
- package/template/console/operations/controller.ts +237 -0
- package/template/console/operations/events.ts +37 -0
- package/template/console/operations/store.ts +32 -0
- package/template/console/operations/types.ts +107 -0
- package/template/console/people/PeopleWorkspace.tsx +81 -0
- package/template/console/people/types.ts +25 -0
- package/template/console/profile/ProfileWorkspace.tsx +136 -0
- package/template/console/profile/events.ts +9 -0
- package/template/console/profile/types.ts +6 -0
- package/template/console/quality/QualityWorkspace.tsx +87 -0
- package/template/console/quality/controller.ts +154 -0
- package/template/console/quality/events.ts +34 -0
- package/template/console/quality/store.ts +36 -0
- package/template/console/quality/types.ts +70 -0
- package/template/console/queues/QueueEditor.tsx +173 -0
- package/template/console/queues/QueueWorkspace.tsx +109 -0
- package/template/console/queues/controller.ts +194 -0
- package/template/console/queues/events.ts +34 -0
- package/template/console/queues/store.ts +37 -0
- package/template/console/queues/types.ts +102 -0
- package/template/console/registry/RegistryDialog.tsx +198 -0
- package/template/console/registry/RegistryWorkspace.tsx +108 -0
- package/template/console/registry/events.ts +14 -0
- package/template/console/registry/types.ts +52 -0
- package/template/console/reports/ReportDrawer.tsx +159 -0
- package/template/console/reports/ReportWorkspace.tsx +90 -0
- package/template/console/reports/controller.ts +517 -0
- package/template/console/reports/events.ts +42 -0
- package/template/console/reports/store.ts +39 -0
- package/template/console/reports/types.ts +230 -0
- package/template/console/settings/BrandEditor.tsx +126 -0
- package/template/console/settings/ChannelDialog.tsx +241 -0
- package/template/console/settings/SettingsWorkspace.tsx +155 -0
- package/template/console/settings/events.ts +19 -0
- package/template/console/settings/handoff.ts +22 -0
- package/template/console/settings/types.ts +93 -0
- package/template/console/shell/WorkspaceShell.tsx +160 -0
- package/template/console/shell/controller.ts +182 -0
- package/template/console/shell/events.ts +25 -0
- package/template/console/shell/navigation.ts +61 -0
- package/template/console/shell/store.ts +56 -0
- package/template/console/shell/types.ts +86 -0
- package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
- package/template/console/workflows/WorkflowCanvas.tsx +42 -0
- package/template/console/workflows/WorkflowDialogs.tsx +6 -0
- package/template/console/workflows/WorkflowStudio.tsx +331 -0
- package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
- package/template/console/workflows/events.ts +17 -0
- package/template/console/workflows/graph.ts +156 -0
- package/template/console/workflows/templates.ts +70 -0
- package/template/console/workflows/types.ts +201 -0
- package/template/gitignore.template +18 -0
- package/template/migrations/0001_reports_foundation.sql +444 -0
- package/template/migrations/0002_human_report_loop.sql +65 -0
- package/template/migrations/0003_delivery_reliability.sql +18 -0
- package/template/migrations/0004_public_intake.sql +14 -0
- package/template/migrations/0005_operations_visibility.sql +24 -0
- package/template/migrations/0006_ai_governance.sql +198 -0
- package/template/migrations/0007_ai_release_gates.sql +6 -0
- package/template/migrations/0008_retention_analytics_exports.sql +52 -0
- package/template/migrations/0009_retention_derived_copies.sql +14 -0
- package/template/migrations/0010_ai_quality_controls.sql +26 -0
- package/template/migrations/0011_analyst_presence.sql +28 -0
- package/template/migrations/0012_queue_policies.sql +87 -0
- package/template/migrations/0013_routing_agents.sql +63 -0
- package/template/migrations/0014_webhook_enrichments.sql +122 -0
- package/template/migrations/0015_queue_owned_ai.sql +55 -0
- package/template/migrations/0016_operator_accounts.sql +67 -0
- package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
- package/template/migrations/0018_platform_configuration.sql +382 -0
- package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
- package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
- package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
- package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
- package/template/migrations/0023_component_connection_execution.sql +75 -0
- package/template/migrations/0024_access_runtime_integrity.sql +72 -0
- package/template/migrations/0025_installation_timezone.sql +11 -0
- package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
- package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
- package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
- package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
- package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
- package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
- package/template/migrations/0032_action_delivery_outbox.sql +55 -0
- package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
- package/template/migrations/0034_installation_integrations.sql +31 -0
- package/template/migrations/0035_workspace_governance.sql +21 -0
- package/template/migrations/0036_published_routing_baseline.sql +15 -0
- package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
- package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
- package/template/migrations/0039_secure_reporting_channels.sql +45 -0
- package/template/migrations/0040_notification_only_reporting.sql +24 -0
- package/template/migrations/0041_better_auth_credentials.sql +17 -0
- package/template/package.json +47 -0
- package/template/public/_headers +27 -0
- package/template/public/app-icon-192.png +0 -0
- package/template/public/app-icon-512.png +0 -0
- package/template/public/brand-icon.svg +7 -0
- package/template/public/brand-tokens.css +80 -0
- package/template/public/console/auth-shell.js +19340 -0
- package/template/public/customer-brand.js +58 -0
- package/template/public/embed/embed.css +139 -0
- package/template/public/embed/embed.js +408 -0
- package/template/public/embed/index.html +91 -0
- package/template/public/favicon.svg +7 -0
- package/template/public/fonts/Manrope-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Variable.ttf +0 -0
- package/template/public/index.html +123 -0
- package/template/public/logo-primary.svg +7 -0
- package/template/public/logo-reversed.svg +7 -0
- package/template/public/manifest.webmanifest +21 -0
- package/template/public/public-report.js +184 -0
- package/template/public/report/index.html +39 -0
- package/template/public/report/public-report.css +20 -0
- package/template/public/social-card.png +0 -0
- package/template/public/styles.css +1521 -0
- package/template/public/widget.css +80 -0
- package/template/public/widget.js +220 -0
- package/template/reports.config.example.json +38 -0
- package/template/reports.schema.json +89 -0
- package/template/scripts/reports-auth-onboarding.mjs +180 -0
- package/template/scripts/reports-backup.mjs +276 -0
- package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
- package/template/scripts/reports-deploy.mjs +173 -0
- package/template/scripts/reports-plan.mjs +226 -0
- package/template/scripts/reports-restore.mjs +180 -0
- package/template/scripts/reports-secrets.mjs +73 -0
- package/template/scripts/reports-uninstall-plan.mjs +32 -0
- package/template/src/agent-executor.ts +330 -0
- package/template/src/ai-observability.ts +163 -0
- package/template/src/ai-registry-validation.ts +244 -0
- package/template/src/ai-registry.ts +292 -0
- package/template/src/api-cursor.ts +84 -0
- package/template/src/assistant.ts +554 -0
- package/template/src/audit.ts +39 -0
- package/template/src/backup-service.ts +269 -0
- package/template/src/budget-control.ts +127 -0
- package/template/src/component-executor.ts +845 -0
- package/template/src/configuration-registry.ts +550 -0
- package/template/src/connection-egress.ts +430 -0
- package/template/src/connection-oauth.ts +368 -0
- package/template/src/evidence-service.ts +335 -0
- package/template/src/human-tasks.ts +316 -0
- package/template/src/index.ts +4114 -0
- package/template/src/installation-admin.ts +301 -0
- package/template/src/intake-abuse.ts +156 -0
- package/template/src/platform-registry-validation.ts +367 -0
- package/template/src/platform-registry.ts +753 -0
- package/template/src/report-admin.ts +342 -0
- package/template/src/report-ai-quality.ts +390 -0
- package/template/src/report-ai-validation.ts +80 -0
- package/template/src/report-ai.ts +857 -0
- package/template/src/report-auth.ts +292 -0
- package/template/src/report-better-auth.ts +583 -0
- package/template/src/report-context-schema.ts +132 -0
- package/template/src/report-context.ts +126 -0
- package/template/src/report-crypto.ts +142 -0
- package/template/src/report-delivery.ts +579 -0
- package/template/src/report-form-validation.ts +51 -0
- package/template/src/report-governance.ts +510 -0
- package/template/src/report-http.ts +90 -0
- package/template/src/report-operations.ts +685 -0
- package/template/src/report-operator-accounts.ts +810 -0
- package/template/src/report-presence.ts +398 -0
- package/template/src/report-queue-validation.ts +309 -0
- package/template/src/report-queues.ts +478 -0
- package/template/src/report-repository.ts +972 -0
- package/template/src/report-router-agent.ts +328 -0
- package/template/src/report-router-validation.ts +81 -0
- package/template/src/report-routing.ts +178 -0
- package/template/src/report-turnstile.ts +142 -0
- package/template/src/report-types.ts +224 -0
- package/template/src/report-validation.ts +279 -0
- package/template/src/report-workflow-validation.ts +119 -0
- package/template/src/report-workflow.ts +886 -0
- package/template/src/shadow-quality.ts +278 -0
- package/template/src/workflow-actions.ts +782 -0
- package/template/src/workflow-compiler.ts +230 -0
- package/template/src/workflow-dynamic-runtime.ts +613 -0
- package/template/src/workflow-effects.ts +316 -0
- package/template/src/workflow-expressions.ts +191 -0
- package/template/src/workflow-platform-types.ts +121 -0
- package/template/src/workflow-platform-validation.ts +540 -0
- package/template/src/workflow-repository.ts +916 -0
- package/template/src/workflow-runs.ts +686 -0
- package/template/src/workflow-simulator.ts +211 -0
- package/template/src/workspace-branding.ts +128 -0
- package/template/src/workspace-governance.ts +279 -0
- package/template/tsconfig.console.json +23 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.console.config.ts +20 -0
- package/template/worker-configuration.d.ts +65 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { ApiError } from "./report-http";
|
|
2
|
+
|
|
3
|
+
export type QueueManagementMode = "human" | "ai" | "hybrid";
|
|
4
|
+
export type QueueRolloutMode = "off" | "shadow" | "assist" | "autonomous_low_priority";
|
|
5
|
+
export type QueueStatus = "active" | "paused" | "archived";
|
|
6
|
+
|
|
7
|
+
export interface RoutingCondition {
|
|
8
|
+
field: string;
|
|
9
|
+
operator: "eq" | "neq" | "in" | "not_in" | "exists" | "gt" | "gte" | "lt" | "lte" | "contains";
|
|
10
|
+
value?: string | number | boolean | null | Array<string | number | boolean>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QueuePolicyInput {
|
|
14
|
+
managementMode: QueueManagementMode;
|
|
15
|
+
rolloutMode: QueueRolloutMode;
|
|
16
|
+
priority: number;
|
|
17
|
+
responseSlaMinutes: number | null;
|
|
18
|
+
resolutionSlaMinutes: number | null;
|
|
19
|
+
claimTtlMinutes: number;
|
|
20
|
+
fallbackQueueId: string | null;
|
|
21
|
+
reviewerInstructions: string;
|
|
22
|
+
aiInstructions: string | null;
|
|
23
|
+
aiBounds: {
|
|
24
|
+
allowedOutcomes: string[];
|
|
25
|
+
confidenceThreshold: number;
|
|
26
|
+
maxFollowUpTurns: number;
|
|
27
|
+
autonomousActions: false;
|
|
28
|
+
model: string;
|
|
29
|
+
policyVersionId: string;
|
|
30
|
+
allowedFields: string[];
|
|
31
|
+
allowedTemplateIds: string[];
|
|
32
|
+
maxWaitHours: number;
|
|
33
|
+
dailyReportBudget: number;
|
|
34
|
+
dailyCostBudgetMicrousd: number;
|
|
35
|
+
inputCostMicrousdPerMillion: number;
|
|
36
|
+
outputCostMicrousdPerMillion: number;
|
|
37
|
+
qualitySampleRate: number;
|
|
38
|
+
minimumShadowRuns: number;
|
|
39
|
+
minimumQualityReviews: number;
|
|
40
|
+
maximumFailureRate: number;
|
|
41
|
+
maximumDisagreementRate: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface QueueCreateInput {
|
|
46
|
+
key: string;
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
position: number;
|
|
50
|
+
policy: QueuePolicyInput;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface QueueUpdateInput {
|
|
54
|
+
name?: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
position?: number;
|
|
57
|
+
status?: QueueStatus;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const queueKeyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
61
|
+
const referencePattern = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/u;
|
|
62
|
+
const criterionFieldPattern = /^(?:reason_code|source|target_type|priority|safeguard\.requires_human|trusted_fact\.[A-Za-z0-9_.:-]{1,160}|answer\.[A-Za-z0-9_.:-]{1,160}|enrichment\.[A-Za-z0-9_.:-]{1,160})$/u;
|
|
63
|
+
const managementModes = new Set<QueueManagementMode>(["human", "ai", "hybrid"]);
|
|
64
|
+
const rolloutModes = new Map<string, QueueRolloutMode>([
|
|
65
|
+
["off", "off"], ["shadow", "shadow"], ["assist", "assist"],
|
|
66
|
+
["autonomous", "autonomous_low_priority"], ["autonomous_low_priority", "autonomous_low_priority"],
|
|
67
|
+
]);
|
|
68
|
+
const queueStatuses = new Set<QueueStatus>(["active", "paused", "archived"]);
|
|
69
|
+
const routingOperators = new Set<RoutingCondition["operator"]>(["eq", "neq", "in", "not_in", "exists", "gt", "gte", "lt", "lte", "contains"]);
|
|
70
|
+
const aiFields = new Set(["target.type", "answers.details", "evidence.references", "enrichments"]);
|
|
71
|
+
const aiOutcomes = new Set(["out_of_scope", "insufficient_information", "no_violation_found", "escalate_to_human"]);
|
|
72
|
+
const aiTemplates = new Set(["ai-more-context-v1", "ai-no-violation-v1", "ai-out-of-scope-v1"]);
|
|
73
|
+
|
|
74
|
+
function object(value: unknown, name: string): Record<string, unknown> {
|
|
75
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
76
|
+
throw new ApiError(400, "invalid_queue_configuration", `${name} must be an object.`);
|
|
77
|
+
}
|
|
78
|
+
return value as Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function knownFields(value: Record<string, unknown>, allowed: readonly string[], name: string): void {
|
|
82
|
+
const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
|
|
83
|
+
if (unexpected) throw new ApiError(400, "invalid_queue_configuration", `${name} contains unsupported field ${unexpected}.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stringValue(value: unknown, name: string, minimum: number, maximum: number): string {
|
|
87
|
+
if (typeof value !== "string" || value.trim().length < minimum || value.trim().length > maximum) {
|
|
88
|
+
throw new ApiError(400, "invalid_queue_configuration", `${name} must contain ${minimum} to ${maximum} characters.`);
|
|
89
|
+
}
|
|
90
|
+
return value.trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function integer(value: unknown, name: string, minimum: number, maximum: number): number {
|
|
94
|
+
if (!Number.isInteger(value) || Number(value) < minimum || Number(value) > maximum) {
|
|
95
|
+
throw new ApiError(400, "invalid_queue_configuration", `${name} must be an integer from ${minimum} to ${maximum}.`);
|
|
96
|
+
}
|
|
97
|
+
return Number(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function numeric(value: unknown, name: string, minimum: number, maximum: number): number {
|
|
101
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) {
|
|
102
|
+
throw new ApiError(400, "invalid_queue_configuration", `${name} must be a number from ${minimum} to ${maximum}.`);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function nullableInteger(value: unknown, name: string, minimum: number, maximum: number): number | null {
|
|
108
|
+
return value === null || value === undefined ? null : integer(value, name, minimum, maximum);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function identifier(value: unknown, name: string): string {
|
|
112
|
+
const parsed = stringValue(value, name, 1, 200);
|
|
113
|
+
if (!referencePattern.test(parsed)) throw new ApiError(400, "invalid_queue_configuration", `${name} is invalid.`);
|
|
114
|
+
return parsed;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function routingValue(value: unknown, name: string): RoutingCondition["value"] {
|
|
118
|
+
if (value === null || typeof value === "boolean") return value;
|
|
119
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
120
|
+
if (typeof value === "string" && value.length <= 500) return value;
|
|
121
|
+
if (Array.isArray(value) && value.length > 0 && value.length <= 50) {
|
|
122
|
+
return value.map((item, index) => {
|
|
123
|
+
if (typeof item === "boolean") return item;
|
|
124
|
+
if (typeof item === "number" && Number.isFinite(item)) return item;
|
|
125
|
+
if (typeof item === "string" && item.length <= 500) return item;
|
|
126
|
+
throw new ApiError(400, "invalid_queue_configuration", `${name}[${index}] is invalid.`);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
throw new ApiError(400, "invalid_queue_configuration", `${name} is invalid.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function parseRoutingCriteria(value: unknown): { match: "all" | "any"; conditions: RoutingCondition[] } {
|
|
133
|
+
const input = object(value, "routing_criteria");
|
|
134
|
+
knownFields(input, ["match", "conditions"], "routing_criteria");
|
|
135
|
+
if (input.match !== "all" && input.match !== "any") {
|
|
136
|
+
throw new ApiError(400, "invalid_queue_configuration", "routing_criteria.match must be all or any.");
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(input.conditions) || input.conditions.length > 50) {
|
|
139
|
+
throw new ApiError(400, "invalid_queue_configuration", "routing_criteria.conditions must contain at most 50 conditions.");
|
|
140
|
+
}
|
|
141
|
+
const conditions = input.conditions.map((value, index): RoutingCondition => {
|
|
142
|
+
const condition = object(value, `routing_criteria.conditions[${index}]`);
|
|
143
|
+
knownFields(condition, ["field", "operator", "value"], `routing_criteria.conditions[${index}]`);
|
|
144
|
+
const field = stringValue(condition.field, `routing_criteria.conditions[${index}].field`, 1, 200);
|
|
145
|
+
if (!criterionFieldPattern.test(field)) {
|
|
146
|
+
throw new ApiError(400, "invalid_queue_configuration", `routing_criteria.conditions[${index}].field is not an allowed report, safeguard, or enrichment field.`);
|
|
147
|
+
}
|
|
148
|
+
if (typeof condition.operator !== "string" || !routingOperators.has(condition.operator as RoutingCondition["operator"])) {
|
|
149
|
+
throw new ApiError(400, "invalid_queue_configuration", `routing_criteria.conditions[${index}].operator is invalid.`);
|
|
150
|
+
}
|
|
151
|
+
const operator = condition.operator as RoutingCondition["operator"];
|
|
152
|
+
if (operator === "exists") {
|
|
153
|
+
if (condition.value !== undefined) throw new ApiError(400, "invalid_queue_configuration", `routing_criteria.conditions[${index}].value is not used with exists.`);
|
|
154
|
+
return { field, operator };
|
|
155
|
+
}
|
|
156
|
+
if (condition.value === undefined) throw new ApiError(400, "invalid_queue_configuration", `routing_criteria.conditions[${index}].value is required.`);
|
|
157
|
+
const parsedValue = routingValue(condition.value, `routing_criteria.conditions[${index}].value`);
|
|
158
|
+
if ((operator === "in" || operator === "not_in") && !Array.isArray(parsedValue)) {
|
|
159
|
+
throw new ApiError(400, "invalid_queue_configuration", `${operator} requires an array value.`);
|
|
160
|
+
}
|
|
161
|
+
return { field, operator, value: parsedValue };
|
|
162
|
+
});
|
|
163
|
+
return { match: input.match, conditions };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function stringList(value: unknown, name: string, maximum: number, allowed?: Set<string>): string[] {
|
|
167
|
+
if (!Array.isArray(value) || value.length > maximum) {
|
|
168
|
+
throw new ApiError(400, "invalid_queue_configuration", `${name} must contain at most ${maximum} identifiers.`);
|
|
169
|
+
}
|
|
170
|
+
const parsed = value.map((item, index) => identifier(item, `${name}[${index}]`));
|
|
171
|
+
if (new Set(parsed).size !== parsed.length) throw new ApiError(400, "invalid_queue_configuration", `${name} contains duplicates.`);
|
|
172
|
+
if (allowed && parsed.some((item) => !allowed.has(item))) {
|
|
173
|
+
throw new ApiError(400, "invalid_queue_configuration", `${name} contains an unsupported value.`);
|
|
174
|
+
}
|
|
175
|
+
return parsed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseAiBounds(value: unknown): QueuePolicyInput["aiBounds"] {
|
|
179
|
+
const input = object(value ?? {}, "ai_bounds");
|
|
180
|
+
knownFields(input, [
|
|
181
|
+
"allowed_outcomes", "confidence_threshold", "max_follow_up_turns", "autonomous_actions",
|
|
182
|
+
"model", "policy_version_id", "allowed_fields", "allowed_template_ids", "max_wait_hours",
|
|
183
|
+
"daily_report_budget", "daily_cost_budget_microusd", "input_cost_microusd_per_million",
|
|
184
|
+
"output_cost_microusd_per_million", "quality_sample_rate", "minimum_shadow_runs",
|
|
185
|
+
"minimum_quality_reviews", "maximum_failure_rate", "maximum_disagreement_rate",
|
|
186
|
+
], "ai_bounds");
|
|
187
|
+
if (input.autonomous_actions !== undefined && input.autonomous_actions !== false) {
|
|
188
|
+
throw new ApiError(400, "autonomous_actions_not_supported", "Autonomous customer application actions are disabled in the first release.");
|
|
189
|
+
}
|
|
190
|
+
const confidence = input.confidence_threshold ?? 0.85;
|
|
191
|
+
if (typeof confidence !== "number" || !Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
192
|
+
throw new ApiError(400, "invalid_queue_configuration", "ai_bounds.confidence_threshold must be from 0 to 1.");
|
|
193
|
+
}
|
|
194
|
+
const model = stringValue(input.model ?? "@cf/meta/llama-3.1-8b-instruct-fast", "ai_bounds.model", 3, 200);
|
|
195
|
+
if (!model.startsWith("@cf/")) throw new ApiError(400, "invalid_queue_configuration", "ai_bounds.model must be a Workers AI @cf model.");
|
|
196
|
+
const allowedOutcomes = stringList(input.allowed_outcomes ?? [
|
|
197
|
+
"no_violation_found", "out_of_scope", "insufficient_information", "escalate_to_human",
|
|
198
|
+
], "ai_bounds.allowed_outcomes", 50, aiOutcomes);
|
|
199
|
+
return {
|
|
200
|
+
allowedOutcomes,
|
|
201
|
+
confidenceThreshold: confidence,
|
|
202
|
+
maxFollowUpTurns: integer(input.max_follow_up_turns ?? 0, "ai_bounds.max_follow_up_turns", 0, 10),
|
|
203
|
+
autonomousActions: false,
|
|
204
|
+
model,
|
|
205
|
+
policyVersionId: identifier(input.policy_version_id ?? "general-policy-v1", "ai_bounds.policy_version_id"),
|
|
206
|
+
allowedFields: stringList(input.allowed_fields ?? ["target.type", "answers.details", "evidence.references", "enrichments"], "ai_bounds.allowed_fields", 20, aiFields),
|
|
207
|
+
allowedTemplateIds: stringList(input.allowed_template_ids ?? ["ai-more-context-v1", "ai-no-violation-v1", "ai-out-of-scope-v1"], "ai_bounds.allowed_template_ids", 20, aiTemplates),
|
|
208
|
+
maxWaitHours: integer(input.max_wait_hours ?? 168, "ai_bounds.max_wait_hours", 1, 720),
|
|
209
|
+
dailyReportBudget: integer(input.daily_report_budget ?? 100, "ai_bounds.daily_report_budget", 0, 1_000_000),
|
|
210
|
+
dailyCostBudgetMicrousd: integer(input.daily_cost_budget_microusd ?? 1_000_000, "ai_bounds.daily_cost_budget_microusd", 0, 1_000_000_000),
|
|
211
|
+
inputCostMicrousdPerMillion: integer(input.input_cost_microusd_per_million ?? 0, "ai_bounds.input_cost_microusd_per_million", 0, 1_000_000_000),
|
|
212
|
+
outputCostMicrousdPerMillion: integer(input.output_cost_microusd_per_million ?? 0, "ai_bounds.output_cost_microusd_per_million", 0, 1_000_000_000),
|
|
213
|
+
qualitySampleRate: numeric(input.quality_sample_rate ?? 0.1, "ai_bounds.quality_sample_rate", 0, 1),
|
|
214
|
+
minimumShadowRuns: integer(input.minimum_shadow_runs ?? 25, "ai_bounds.minimum_shadow_runs", 1, 100_000),
|
|
215
|
+
minimumQualityReviews: integer(input.minimum_quality_reviews ?? 10, "ai_bounds.minimum_quality_reviews", 0, 100_000),
|
|
216
|
+
maximumFailureRate: numeric(input.maximum_failure_rate ?? 0.1, "ai_bounds.maximum_failure_rate", 0, 1),
|
|
217
|
+
maximumDisagreementRate: numeric(input.maximum_disagreement_rate ?? 0.1, "ai_bounds.maximum_disagreement_rate", 0, 1),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function parseQueuePolicy(value: unknown): QueuePolicyInput {
|
|
222
|
+
const input = object(value, "policy");
|
|
223
|
+
knownFields(input, [
|
|
224
|
+
"management_mode", "rollout_mode", "priority", "response_sla_minutes", "resolution_sla_minutes",
|
|
225
|
+
"claim_ttl_minutes", "fallback_queue_id", "reviewer_instructions", "ai_instructions", "ai_bounds",
|
|
226
|
+
], "policy");
|
|
227
|
+
if (typeof input.management_mode !== "string" || !managementModes.has(input.management_mode as QueueManagementMode)) {
|
|
228
|
+
throw new ApiError(400, "invalid_queue_configuration", "policy.management_mode must be human, ai, or hybrid.");
|
|
229
|
+
}
|
|
230
|
+
const managementMode = input.management_mode as QueueManagementMode;
|
|
231
|
+
const rolloutMode = typeof input.rollout_mode === "string" ? rolloutModes.get(input.rollout_mode) : undefined;
|
|
232
|
+
if (!rolloutMode) throw new ApiError(400, "invalid_queue_configuration", "policy.rollout_mode is invalid.");
|
|
233
|
+
if (managementMode === "human" && rolloutMode !== "off") {
|
|
234
|
+
throw new ApiError(400, "invalid_queue_configuration", "Human-managed queues must use the off automation rollout.");
|
|
235
|
+
}
|
|
236
|
+
const aiInstructions = input.ai_instructions === null || input.ai_instructions === undefined
|
|
237
|
+
? null
|
|
238
|
+
: stringValue(input.ai_instructions, "policy.ai_instructions", 10, 10_000);
|
|
239
|
+
if (managementMode !== "human" && !aiInstructions) {
|
|
240
|
+
throw new ApiError(400, "invalid_queue_configuration", "AI and hybrid queues require policy.ai_instructions.");
|
|
241
|
+
}
|
|
242
|
+
const aiBounds = parseAiBounds(input.ai_bounds);
|
|
243
|
+
if (managementMode !== "human" && rolloutMode !== "off") {
|
|
244
|
+
if (!aiBounds.allowedOutcomes.length || !aiBounds.dailyReportBudget || !aiBounds.dailyCostBudgetMicrousd) {
|
|
245
|
+
throw new ApiError(400, "invalid_queue_configuration", "Enabled AI and hybrid queues require allowed outcomes plus non-zero daily report and cost budgets.");
|
|
246
|
+
}
|
|
247
|
+
const requiredTemplates = new Map([
|
|
248
|
+
["insufficient_information", "ai-more-context-v1"],
|
|
249
|
+
["no_violation_found", "ai-no-violation-v1"],
|
|
250
|
+
["out_of_scope", "ai-out-of-scope-v1"],
|
|
251
|
+
]);
|
|
252
|
+
for (const outcome of aiBounds.allowedOutcomes) {
|
|
253
|
+
const template = requiredTemplates.get(outcome);
|
|
254
|
+
if (template && !aiBounds.allowedTemplateIds.includes(template)) {
|
|
255
|
+
throw new ApiError(400, "invalid_queue_configuration", `${outcome} requires approved template ${template}.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (rolloutMode === "autonomous_low_priority") {
|
|
260
|
+
if (!aiBounds.inputCostMicrousdPerMillion && !aiBounds.outputCostMicrousdPerMillion) {
|
|
261
|
+
throw new ApiError(400, "invalid_queue_configuration", "Autonomous rollout requires configured model unit prices for cost enforcement.");
|
|
262
|
+
}
|
|
263
|
+
if (aiBounds.qualitySampleRate < 0.05) {
|
|
264
|
+
throw new ApiError(400, "invalid_queue_configuration", "Autonomous rollout requires at least a 5% human quality sample.");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
managementMode,
|
|
269
|
+
rolloutMode,
|
|
270
|
+
priority: integer(input.priority ?? 50, "policy.priority", 0, 100),
|
|
271
|
+
responseSlaMinutes: nullableInteger(input.response_sla_minutes, "policy.response_sla_minutes", 1, 525_600),
|
|
272
|
+
resolutionSlaMinutes: nullableInteger(input.resolution_sla_minutes, "policy.resolution_sla_minutes", 1, 525_600),
|
|
273
|
+
claimTtlMinutes: integer(input.claim_ttl_minutes ?? 15, "policy.claim_ttl_minutes", 1, 1_440),
|
|
274
|
+
fallbackQueueId: input.fallback_queue_id === null || input.fallback_queue_id === undefined
|
|
275
|
+
? null : identifier(input.fallback_queue_id, "policy.fallback_queue_id"),
|
|
276
|
+
reviewerInstructions: stringValue(input.reviewer_instructions, "policy.reviewer_instructions", 10, 10_000),
|
|
277
|
+
aiInstructions,
|
|
278
|
+
aiBounds,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function parseQueueCreate(value: Record<string, unknown>): QueueCreateInput {
|
|
283
|
+
knownFields(value, ["key", "name", "description", "position", "policy"], "queue");
|
|
284
|
+
const key = stringValue(value.key, "key", 2, 80);
|
|
285
|
+
if (!queueKeyPattern.test(key)) throw new ApiError(400, "invalid_queue_configuration", "key must be a lowercase kebab-case identifier.");
|
|
286
|
+
return {
|
|
287
|
+
key,
|
|
288
|
+
name: stringValue(value.name, "name", 2, 100),
|
|
289
|
+
description: value.description === undefined ? "" : stringValue(value.description, "description", 0, 1_000),
|
|
290
|
+
position: integer(value.position ?? 100, "position", 0, 10_000),
|
|
291
|
+
policy: parseQueuePolicy(value.policy),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function parseQueueUpdate(value: Record<string, unknown>): QueueUpdateInput {
|
|
296
|
+
knownFields(value, ["name", "description", "position", "status"], "queue update");
|
|
297
|
+
if (!Object.keys(value).length) throw new ApiError(400, "invalid_queue_configuration", "The queue update is empty.");
|
|
298
|
+
const result: QueueUpdateInput = {};
|
|
299
|
+
if (value.name !== undefined) result.name = stringValue(value.name, "name", 2, 100);
|
|
300
|
+
if (value.description !== undefined) result.description = stringValue(value.description, "description", 0, 1_000);
|
|
301
|
+
if (value.position !== undefined) result.position = integer(value.position, "position", 0, 10_000);
|
|
302
|
+
if (value.status !== undefined) {
|
|
303
|
+
if (typeof value.status !== "string" || !queueStatuses.has(value.status as QueueStatus)) {
|
|
304
|
+
throw new ApiError(400, "invalid_queue_configuration", "status must be active, paused, or archived.");
|
|
305
|
+
}
|
|
306
|
+
result.status = value.status as QueueStatus;
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|