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,478 @@
|
|
|
1
|
+
import { canonicalJson } from "./audit";
|
|
2
|
+
import { ApiError } from "./report-http";
|
|
3
|
+
import type { OperatorSession } from "./report-types";
|
|
4
|
+
import type { QueueCreateInput, QueuePolicyInput, QueueStatus, QueueUpdateInput } from "./report-queue-validation";
|
|
5
|
+
|
|
6
|
+
interface QueueRow {
|
|
7
|
+
id: string;
|
|
8
|
+
key: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
purpose: "reports" | "appeals" | "quality" | "operations";
|
|
12
|
+
status: QueueStatus;
|
|
13
|
+
position: number;
|
|
14
|
+
activeVersionId: string | null;
|
|
15
|
+
createdBy: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
versionId: string | null;
|
|
19
|
+
version: number | null;
|
|
20
|
+
managementMode: QueuePolicyInput["managementMode"] | null;
|
|
21
|
+
rolloutMode: QueuePolicyInput["rolloutMode"] | null;
|
|
22
|
+
priority: number | null;
|
|
23
|
+
responseSlaMinutes: number | null;
|
|
24
|
+
resolutionSlaMinutes: number | null;
|
|
25
|
+
claimTtlMinutes: number | null;
|
|
26
|
+
fallbackQueueId: string | null;
|
|
27
|
+
reviewerInstructions: string | null;
|
|
28
|
+
aiInstructions: string | null;
|
|
29
|
+
aiBoundsJson: string | null;
|
|
30
|
+
versionCreatedBy: string | null;
|
|
31
|
+
versionCreatedAt: string | null;
|
|
32
|
+
publishedAt: string | null;
|
|
33
|
+
openReportCount: number;
|
|
34
|
+
totalReportCount: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface VersionRow {
|
|
38
|
+
id: string;
|
|
39
|
+
queueId: string;
|
|
40
|
+
version: number;
|
|
41
|
+
managementMode: QueuePolicyInput["managementMode"];
|
|
42
|
+
rolloutMode: QueuePolicyInput["rolloutMode"];
|
|
43
|
+
priority: number;
|
|
44
|
+
responseSlaMinutes: number | null;
|
|
45
|
+
resolutionSlaMinutes: number | null;
|
|
46
|
+
claimTtlMinutes: number;
|
|
47
|
+
fallbackQueueId: string | null;
|
|
48
|
+
reviewerInstructions: string;
|
|
49
|
+
aiInstructions: string | null;
|
|
50
|
+
aiBoundsJson: string;
|
|
51
|
+
createdBy: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
publishedAt: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface MutationRow {
|
|
57
|
+
action: string;
|
|
58
|
+
actor_id: string;
|
|
59
|
+
response_json: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const closedStates = new Set(["resolved_by_human", "resolved_by_ai", "closed"]);
|
|
63
|
+
|
|
64
|
+
function now(): string {
|
|
65
|
+
return new Date().toISOString();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function jsonValue<T>(value: string | null, fallback: T): T {
|
|
69
|
+
if (!value) return fallback;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(value) as T;
|
|
72
|
+
} catch {
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function publicRollout(value: QueuePolicyInput["rolloutMode"] | null): "off" | "shadow" | "assist" | "autonomous" | null {
|
|
78
|
+
return value === "autonomous_low_priority" ? "autonomous" : value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function policyFromRow(row: VersionRow | QueueRow): Record<string, unknown> | null {
|
|
82
|
+
const id = "versionId" in row ? row.versionId : row.id;
|
|
83
|
+
if (!id || row.version === null || row.managementMode === null || row.rolloutMode === null) return null;
|
|
84
|
+
return {
|
|
85
|
+
id,
|
|
86
|
+
version: row.version,
|
|
87
|
+
managementMode: row.managementMode,
|
|
88
|
+
rolloutMode: publicRollout(row.rolloutMode),
|
|
89
|
+
priority: row.priority,
|
|
90
|
+
responseSlaMinutes: row.responseSlaMinutes,
|
|
91
|
+
resolutionSlaMinutes: row.resolutionSlaMinutes,
|
|
92
|
+
claimTtlMinutes: row.claimTtlMinutes,
|
|
93
|
+
fallbackQueueId: row.fallbackQueueId,
|
|
94
|
+
reviewerInstructions: row.reviewerInstructions,
|
|
95
|
+
aiInstructions: row.aiInstructions,
|
|
96
|
+
aiBounds: jsonValue(row.aiBoundsJson, {}),
|
|
97
|
+
createdBy: "versionCreatedBy" in row ? row.versionCreatedBy : row.createdBy,
|
|
98
|
+
createdAt: "versionCreatedAt" in row ? row.versionCreatedAt : row.createdAt,
|
|
99
|
+
publishedAt: row.publishedAt,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function queueFromRow(row: QueueRow): Record<string, unknown> {
|
|
104
|
+
return {
|
|
105
|
+
id: row.id,
|
|
106
|
+
key: row.key,
|
|
107
|
+
name: row.name,
|
|
108
|
+
description: row.description,
|
|
109
|
+
purpose: row.purpose,
|
|
110
|
+
status: row.status,
|
|
111
|
+
position: row.position,
|
|
112
|
+
createdBy: row.createdBy,
|
|
113
|
+
createdAt: row.createdAt,
|
|
114
|
+
updatedAt: row.updatedAt,
|
|
115
|
+
activePolicy: policyFromRow(row),
|
|
116
|
+
counts: { openReports: row.openReportCount, totalReports: row.totalReportCount },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function mutationReplay(
|
|
121
|
+
db: D1Database,
|
|
122
|
+
idempotencyKey: string,
|
|
123
|
+
action: string,
|
|
124
|
+
actorId: string,
|
|
125
|
+
): Promise<Record<string, unknown> | null> {
|
|
126
|
+
const row = await db.prepare(`
|
|
127
|
+
SELECT action, actor_id, response_json FROM configuration_mutations WHERE idempotency_key = ?1 LIMIT 1
|
|
128
|
+
`).bind(idempotencyKey).first<MutationRow>();
|
|
129
|
+
if (!row) return null;
|
|
130
|
+
if (row.action !== action || row.actor_id !== actorId) {
|
|
131
|
+
throw new ApiError(409, "idempotency_key_reused", "This Idempotency-Key was already used for another configuration mutation.");
|
|
132
|
+
}
|
|
133
|
+
return jsonValue<Record<string, unknown>>(row.response_json, {});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function requireHumanFallback(db: D1Database, policy: QueuePolicyInput, queueId?: string): Promise<void> {
|
|
137
|
+
if (policy.managementMode === "human" || policy.rolloutMode === "off") return;
|
|
138
|
+
if (!policy.fallbackQueueId) {
|
|
139
|
+
throw new ApiError(400, "human_fallback_required", "An enabled AI-managed queue requires an active human-managed fallback queue.");
|
|
140
|
+
}
|
|
141
|
+
if (policy.fallbackQueueId === queueId) {
|
|
142
|
+
throw new ApiError(400, "invalid_fallback_queue", "A queue cannot use itself as its human fallback.");
|
|
143
|
+
}
|
|
144
|
+
const fallback = await db.prepare(`
|
|
145
|
+
SELECT q.id
|
|
146
|
+
FROM queue_definitions q
|
|
147
|
+
JOIN queue_versions v ON v.id = q.active_version_id
|
|
148
|
+
WHERE q.id = ?1 AND q.purpose = 'reports' AND q.status = 'active' AND v.management_mode = 'human'
|
|
149
|
+
LIMIT 1
|
|
150
|
+
`).bind(policy.fallbackQueueId).first<{ id: string }>();
|
|
151
|
+
if (!fallback) throw new ApiError(400, "invalid_fallback_queue", "The fallback must be an active human-managed report queue.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function requireAiPolicyResources(db: D1Database, policy: QueuePolicyInput): Promise<void> {
|
|
155
|
+
if (policy.managementMode === "human" || policy.rolloutMode === "off") return;
|
|
156
|
+
const adjudicationPolicy = await db.prepare(`
|
|
157
|
+
SELECT id FROM policy_versions WHERE id = ?1 AND published_at IS NOT NULL LIMIT 1
|
|
158
|
+
`).bind(policy.aiBounds.policyVersionId).first<{ id: string }>();
|
|
159
|
+
if (!adjudicationPolicy) {
|
|
160
|
+
throw new ApiError(400, "ai_policy_not_published", "The queue's adjudication policy version is not published.");
|
|
161
|
+
}
|
|
162
|
+
for (const templateId of policy.aiBounds.allowedTemplateIds) {
|
|
163
|
+
const template = await db.prepare(`
|
|
164
|
+
SELECT id FROM message_template_versions
|
|
165
|
+
WHERE id = ?1 AND audience = 'reporter' AND automation_allowed = 1 AND published_at IS NOT NULL
|
|
166
|
+
LIMIT 1
|
|
167
|
+
`).bind(templateId).first<{ id: string }>();
|
|
168
|
+
if (!template) throw new ApiError(400, "ai_template_not_approved", `Template ${templateId} is not approved for automated reporter messaging.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function requireAutonomousReleaseGate(db: D1Database, queueId: string, policy: QueuePolicyInput): Promise<void> {
|
|
173
|
+
if (policy.rolloutMode !== "autonomous_low_priority") return;
|
|
174
|
+
const shadow = await db.prepare(`
|
|
175
|
+
SELECT COUNT(*) AS total,
|
|
176
|
+
SUM(CASE WHEN ar.status IN ('failed', 'invalid') THEN 1 ELSE 0 END) AS failures
|
|
177
|
+
FROM ai_runs ar JOIN ai_queue_config_versions c ON c.id = ar.config_version_id
|
|
178
|
+
WHERE c.queue_id = ?1 AND c.mode = 'shadow' AND c.model = ?2 AND c.policy_version_id = ?3
|
|
179
|
+
`).bind(queueId, policy.aiBounds.model, policy.aiBounds.policyVersionId)
|
|
180
|
+
.first<{ total: number; failures: number | null }>();
|
|
181
|
+
const total = Number(shadow?.total ?? 0);
|
|
182
|
+
const failureRate = total ? Number(shadow?.failures ?? 0) / total : 1;
|
|
183
|
+
if (total < policy.aiBounds.minimumShadowRuns || failureRate > policy.aiBounds.maximumFailureRate) {
|
|
184
|
+
throw new ApiError(409, "ai_shadow_gate_not_met", "Autonomous rollout has not met this queue policy's shadow volume and failure-rate gate.", {
|
|
185
|
+
observed_runs: total,
|
|
186
|
+
required_runs: policy.aiBounds.minimumShadowRuns,
|
|
187
|
+
observed_failure_rate: failureRate,
|
|
188
|
+
maximum_failure_rate: policy.aiBounds.maximumFailureRate,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const quality = await db.prepare(`
|
|
192
|
+
SELECT COUNT(*) AS total, SUM(CASE WHEN qr.agrees = 0 THEN 1 ELSE 0 END) AS disagreements
|
|
193
|
+
FROM quality_reviews qr JOIN ai_quality_samples qs ON qs.id = qr.sample_id
|
|
194
|
+
JOIN ai_runs ar ON ar.id = qs.ai_run_id
|
|
195
|
+
JOIN ai_queue_config_versions c ON c.id = ar.config_version_id
|
|
196
|
+
WHERE c.queue_id = ?1 AND c.model = ?2 AND c.policy_version_id = ?3
|
|
197
|
+
`).bind(queueId, policy.aiBounds.model, policy.aiBounds.policyVersionId)
|
|
198
|
+
.first<{ total: number; disagreements: number | null }>();
|
|
199
|
+
const reviewed = Number(quality?.total ?? 0);
|
|
200
|
+
const disagreementRate = reviewed ? Number(quality?.disagreements ?? 0) / reviewed : 0;
|
|
201
|
+
if (reviewed < policy.aiBounds.minimumQualityReviews || disagreementRate > policy.aiBounds.maximumDisagreementRate) {
|
|
202
|
+
throw new ApiError(409, "ai_quality_gate_not_met", "Autonomous rollout has not met this queue policy's human quality-review gate.", {
|
|
203
|
+
observed_reviews: reviewed,
|
|
204
|
+
required_reviews: policy.aiBounds.minimumQualityReviews,
|
|
205
|
+
observed_disagreement_rate: disagreementRate,
|
|
206
|
+
maximum_disagreement_rate: policy.aiBounds.maximumDisagreementRate,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function auditEvent(
|
|
212
|
+
db: D1Database,
|
|
213
|
+
input: { key: string; action: string; actorType: OperatorSession["actor"]["type"]; actorId: string; targetId: string; details: unknown; createdAt: string },
|
|
214
|
+
): D1PreparedStatement {
|
|
215
|
+
return db.prepare(`
|
|
216
|
+
INSERT OR IGNORE INTO audit_events (
|
|
217
|
+
id, idempotency_key, action, actor_type, actor_id, target_type, target_id, details_json, created_at
|
|
218
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, 'queue', ?6, ?7, ?8)
|
|
219
|
+
`).bind(crypto.randomUUID(), input.key, input.action, input.actorType, input.actorId, input.targetId, canonicalJson(input.details), input.createdAt);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function versionInsert(db: D1Database, queueId: string, versionId: string, version: number, policy: QueuePolicyInput, actorId: string, createdAt: string): D1PreparedStatement {
|
|
223
|
+
return db.prepare(`
|
|
224
|
+
INSERT INTO queue_versions (
|
|
225
|
+
id, queue_id, version, mode, priority, response_sla_minutes, resolution_sla_minutes,
|
|
226
|
+
claim_ttl_minutes, fallback_queue_id, configuration_json, published_at, created_by, created_at,
|
|
227
|
+
management_mode, reviewer_instructions, ai_instructions, ai_bounds_json
|
|
228
|
+
) VALUES (
|
|
229
|
+
?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?11,
|
|
230
|
+
?13, ?14, ?15, ?16
|
|
231
|
+
)
|
|
232
|
+
`).bind(
|
|
233
|
+
versionId, queueId, version, policy.rolloutMode, policy.priority, policy.responseSlaMinutes,
|
|
234
|
+
policy.resolutionSlaMinutes, policy.claimTtlMinutes, policy.fallbackQueueId,
|
|
235
|
+
canonicalJson({ schemaVersion: 2 }), createdAt, actorId, policy.managementMode,
|
|
236
|
+
policy.reviewerInstructions, policy.aiInstructions, canonicalJson(policy.aiBounds),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function aiRuntimeInsert(
|
|
241
|
+
db: D1Database,
|
|
242
|
+
queueId: string,
|
|
243
|
+
queueVersionId: string,
|
|
244
|
+
policy: QueuePolicyInput,
|
|
245
|
+
actorId: string,
|
|
246
|
+
createdAt: string,
|
|
247
|
+
): D1PreparedStatement | null {
|
|
248
|
+
if (policy.managementMode === "human" || policy.rolloutMode === "off" || !policy.fallbackQueueId) return null;
|
|
249
|
+
return db.prepare(`
|
|
250
|
+
INSERT INTO ai_queue_config_versions (
|
|
251
|
+
id, queue_id, queue_version_id, version, mode, enabled, provider, model,
|
|
252
|
+
input_cost_microusd_per_million, output_cost_microusd_per_million,
|
|
253
|
+
prompt_version, policy_version_id, eligible_form_versions_json, eligible_sources_json,
|
|
254
|
+
eligible_reason_codes_json, eligible_target_types_json, allowed_fields_json, allowed_outcomes_json,
|
|
255
|
+
allowed_action_codes_json, allowed_template_ids_json, confidence_threshold, max_follow_up_turns,
|
|
256
|
+
max_wait_hours, daily_report_budget, daily_cost_budget_microusd, quality_sample_rate,
|
|
257
|
+
minimum_shadow_runs, minimum_quality_reviews, maximum_failure_rate, maximum_disagreement_rate,
|
|
258
|
+
fallback_queue_id, pause_state, published_at, created_by, created_at, idempotency_key
|
|
259
|
+
) VALUES (
|
|
260
|
+
?1, ?2, ?3, (SELECT COALESCE(MAX(version), 0) + 1 FROM ai_queue_config_versions WHERE queue_id = ?2),
|
|
261
|
+
?4, 1, 'workers_ai', ?5, ?6, ?7, ?8, ?9, '["general-v1"]',
|
|
262
|
+
'["signed_widget","public_page","server_api"]', '["harassment","spam","other"]',
|
|
263
|
+
'["message","profile","listing","account","file","transaction","service"]',
|
|
264
|
+
?10, ?11, '[]', ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22,
|
|
265
|
+
?23, 'ready', ?24, ?25, ?24, ?26
|
|
266
|
+
)
|
|
267
|
+
`).bind(
|
|
268
|
+
`${queueVersionId}-ai-runtime`, queueId, queueVersionId, policy.rolloutMode,
|
|
269
|
+
policy.aiBounds.model, policy.aiBounds.inputCostMicrousdPerMillion,
|
|
270
|
+
policy.aiBounds.outputCostMicrousdPerMillion, `queue-policy-${queueVersionId}`,
|
|
271
|
+
policy.aiBounds.policyVersionId, canonicalJson(policy.aiBounds.allowedFields),
|
|
272
|
+
canonicalJson(policy.aiBounds.allowedOutcomes), canonicalJson(policy.aiBounds.allowedTemplateIds),
|
|
273
|
+
policy.aiBounds.confidenceThreshold, policy.aiBounds.maxFollowUpTurns, policy.aiBounds.maxWaitHours,
|
|
274
|
+
policy.aiBounds.dailyReportBudget, policy.aiBounds.dailyCostBudgetMicrousd,
|
|
275
|
+
policy.aiBounds.qualitySampleRate, policy.aiBounds.minimumShadowRuns,
|
|
276
|
+
policy.aiBounds.minimumQualityReviews, policy.aiBounds.maximumFailureRate,
|
|
277
|
+
policy.aiBounds.maximumDisagreementRate, policy.fallbackQueueId, createdAt, actorId,
|
|
278
|
+
`queue-policy-runtime:${queueVersionId}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function listQueues(db: D1Database, includeArchived = false): Promise<Record<string, unknown>[]> {
|
|
283
|
+
const result = await db.prepare(`
|
|
284
|
+
SELECT q.id, q.queue_key AS key, q.name, q.description, q.purpose, q.status, q.position,
|
|
285
|
+
q.active_version_id AS activeVersionId, q.created_by AS createdBy, q.created_at AS createdAt,
|
|
286
|
+
q.updated_at AS updatedAt, v.id AS versionId, v.version, v.management_mode AS managementMode,
|
|
287
|
+
v.mode AS rolloutMode, v.priority, v.response_sla_minutes AS responseSlaMinutes,
|
|
288
|
+
v.resolution_sla_minutes AS resolutionSlaMinutes, v.claim_ttl_minutes AS claimTtlMinutes,
|
|
289
|
+
v.fallback_queue_id AS fallbackQueueId,
|
|
290
|
+
v.reviewer_instructions AS reviewerInstructions, v.ai_instructions AS aiInstructions,
|
|
291
|
+
v.ai_bounds_json AS aiBoundsJson,
|
|
292
|
+
v.created_by AS versionCreatedBy, v.created_at AS versionCreatedAt, v.published_at AS publishedAt,
|
|
293
|
+
SUM(CASE WHEN r.id IS NOT NULL AND r.state NOT IN ('resolved_by_human', 'resolved_by_ai', 'closed') THEN 1 ELSE 0 END) AS openReportCount,
|
|
294
|
+
COUNT(r.id) AS totalReportCount
|
|
295
|
+
FROM queue_definitions q
|
|
296
|
+
LEFT JOIN queue_versions v ON v.id = q.active_version_id
|
|
297
|
+
LEFT JOIN reports r ON r.queue_id = q.id
|
|
298
|
+
WHERE q.purpose = 'reports' AND (?1 = 1 OR q.status != 'archived')
|
|
299
|
+
GROUP BY q.id
|
|
300
|
+
ORDER BY q.position, q.name, q.id
|
|
301
|
+
`).bind(includeArchived ? 1 : 0).all<QueueRow>();
|
|
302
|
+
return result.results.map(queueFromRow);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function queueRow(db: D1Database, queueId: string): Promise<QueueRow | null> {
|
|
306
|
+
return db.prepare(`
|
|
307
|
+
SELECT q.id, q.queue_key AS key, q.name, q.description, q.purpose, q.status, q.position,
|
|
308
|
+
q.active_version_id AS activeVersionId, q.created_by AS createdBy, q.created_at AS createdAt,
|
|
309
|
+
q.updated_at AS updatedAt, v.id AS versionId, v.version, v.management_mode AS managementMode,
|
|
310
|
+
v.mode AS rolloutMode, v.priority, v.response_sla_minutes AS responseSlaMinutes,
|
|
311
|
+
v.resolution_sla_minutes AS resolutionSlaMinutes, v.claim_ttl_minutes AS claimTtlMinutes,
|
|
312
|
+
v.fallback_queue_id AS fallbackQueueId,
|
|
313
|
+
v.reviewer_instructions AS reviewerInstructions, v.ai_instructions AS aiInstructions,
|
|
314
|
+
v.ai_bounds_json AS aiBoundsJson,
|
|
315
|
+
v.created_by AS versionCreatedBy, v.created_at AS versionCreatedAt, v.published_at AS publishedAt,
|
|
316
|
+
SUM(CASE WHEN r.id IS NOT NULL AND r.state NOT IN ('resolved_by_human', 'resolved_by_ai', 'closed') THEN 1 ELSE 0 END) AS openReportCount,
|
|
317
|
+
COUNT(r.id) AS totalReportCount
|
|
318
|
+
FROM queue_definitions q
|
|
319
|
+
LEFT JOIN queue_versions v ON v.id = q.active_version_id
|
|
320
|
+
LEFT JOIN reports r ON r.queue_id = q.id
|
|
321
|
+
WHERE q.id = ?1 AND q.purpose = 'reports'
|
|
322
|
+
GROUP BY q.id
|
|
323
|
+
LIMIT 1
|
|
324
|
+
`).bind(queueId).first<QueueRow>();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function loadQueue(db: D1Database, queueId: string): Promise<Record<string, unknown> | null> {
|
|
328
|
+
const queue = await queueRow(db, queueId);
|
|
329
|
+
if (!queue) return null;
|
|
330
|
+
const versions = await db.prepare(`
|
|
331
|
+
SELECT id, queue_id AS queueId, version, management_mode AS managementMode, mode AS rolloutMode,
|
|
332
|
+
priority, response_sla_minutes AS responseSlaMinutes, resolution_sla_minutes AS resolutionSlaMinutes,
|
|
333
|
+
claim_ttl_minutes AS claimTtlMinutes, fallback_queue_id AS fallbackQueueId,
|
|
334
|
+
reviewer_instructions AS reviewerInstructions, ai_instructions AS aiInstructions,
|
|
335
|
+
ai_bounds_json AS aiBoundsJson, created_by AS createdBy, created_at AS createdAt, published_at AS publishedAt
|
|
336
|
+
FROM queue_versions WHERE queue_id = ?1 ORDER BY version DESC, id DESC
|
|
337
|
+
`).bind(queueId).all<VersionRow>();
|
|
338
|
+
return { ...queueFromRow(queue), versions: versions.results.map(policyFromRow) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function createQueue(
|
|
342
|
+
db: D1Database,
|
|
343
|
+
input: QueueCreateInput,
|
|
344
|
+
session: OperatorSession,
|
|
345
|
+
idempotencyKey: string,
|
|
346
|
+
): Promise<Record<string, unknown>> {
|
|
347
|
+
const replay = await mutationReplay(db, idempotencyKey, "queue.create", session.actor.id);
|
|
348
|
+
if (replay) return { ...replay, idempotentReplay: true };
|
|
349
|
+
await requireHumanFallback(db, input.policy);
|
|
350
|
+
await requireAiPolicyResources(db, input.policy);
|
|
351
|
+
const conflict = await db.prepare(`SELECT id FROM queue_definitions WHERE installation_id = 'default' AND queue_key = ?1 LIMIT 1`)
|
|
352
|
+
.bind(input.key).first<{ id: string }>();
|
|
353
|
+
if (conflict) throw new ApiError(409, "queue_key_exists", "A queue with this key already exists.");
|
|
354
|
+
const queueId = crypto.randomUUID();
|
|
355
|
+
const versionId = crypto.randomUUID();
|
|
356
|
+
const createdAt = now();
|
|
357
|
+
const response = { queueId, policyVersionId: versionId, version: 1 };
|
|
358
|
+
const runtime = aiRuntimeInsert(db, queueId, versionId, input.policy, session.actor.id, createdAt);
|
|
359
|
+
await db.batch([
|
|
360
|
+
db.prepare(`
|
|
361
|
+
INSERT INTO queue_definitions (
|
|
362
|
+
id, installation_id, queue_key, name, queue_kind, enabled, created_at, updated_at,
|
|
363
|
+
description, purpose, status, position, active_version_id, created_by
|
|
364
|
+
) VALUES (?1, 'default', ?2, ?3, ?4, 1, ?5, ?5, ?6, 'reports', 'active', ?7, ?8, ?9)
|
|
365
|
+
`).bind(
|
|
366
|
+
queueId, input.key, input.name, input.policy.managementMode === "ai" ? "ai" : "human",
|
|
367
|
+
createdAt, input.description, input.position, versionId, session.actor.id,
|
|
368
|
+
),
|
|
369
|
+
versionInsert(db, queueId, versionId, 1, input.policy, session.actor.id, createdAt),
|
|
370
|
+
db.prepare(`
|
|
371
|
+
INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at)
|
|
372
|
+
VALUES (?1, 'queue.create', ?2, ?3, ?4, ?5)
|
|
373
|
+
`).bind(idempotencyKey, session.actor.id, queueId, canonicalJson(response), createdAt),
|
|
374
|
+
auditEvent(db, {
|
|
375
|
+
key: `queue-create:${idempotencyKey}`, action: "queue.created", actorType: session.actor.type, actorId: session.actor.id,
|
|
376
|
+
targetId: queueId, details: { key: input.key, policyVersionId: versionId, managementMode: input.policy.managementMode }, createdAt,
|
|
377
|
+
}),
|
|
378
|
+
...(runtime ? [runtime] : []),
|
|
379
|
+
]);
|
|
380
|
+
return { ...response, idempotentReplay: false };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function publishQueuePolicy(
|
|
384
|
+
db: D1Database,
|
|
385
|
+
queueId: string,
|
|
386
|
+
policy: QueuePolicyInput,
|
|
387
|
+
session: OperatorSession,
|
|
388
|
+
idempotencyKey: string,
|
|
389
|
+
): Promise<Record<string, unknown>> {
|
|
390
|
+
const replay = await mutationReplay(db, idempotencyKey, "queue.policy.publish", session.actor.id);
|
|
391
|
+
if (replay) return { ...replay, idempotentReplay: true };
|
|
392
|
+
const queue = await queueRow(db, queueId);
|
|
393
|
+
if (!queue) throw new ApiError(404, "queue_not_found", "The queue does not exist.");
|
|
394
|
+
if (queue.status === "archived") throw new ApiError(409, "queue_archived", "Archived queues cannot receive new policy versions.");
|
|
395
|
+
await requireHumanFallback(db, policy, queueId);
|
|
396
|
+
await requireAiPolicyResources(db, policy);
|
|
397
|
+
await requireAutonomousReleaseGate(db, queueId, policy);
|
|
398
|
+
const latest = await db.prepare(`SELECT COALESCE(MAX(version), 0) AS version FROM queue_versions WHERE queue_id = ?1`)
|
|
399
|
+
.bind(queueId).first<{ version: number }>();
|
|
400
|
+
const version = Number(latest?.version ?? 0) + 1;
|
|
401
|
+
const versionId = crypto.randomUUID();
|
|
402
|
+
const createdAt = now();
|
|
403
|
+
const response = { queueId, policyVersionId: versionId, version };
|
|
404
|
+
const runtime = aiRuntimeInsert(db, queueId, versionId, policy, session.actor.id, createdAt);
|
|
405
|
+
await db.batch([
|
|
406
|
+
versionInsert(db, queueId, versionId, version, policy, session.actor.id, createdAt),
|
|
407
|
+
db.prepare(`
|
|
408
|
+
UPDATE queue_definitions
|
|
409
|
+
SET active_version_id = ?2, queue_kind = ?3, updated_at = ?4
|
|
410
|
+
WHERE id = ?1
|
|
411
|
+
`).bind(queueId, versionId, policy.managementMode === "ai" ? "ai" : "human", createdAt),
|
|
412
|
+
db.prepare(`
|
|
413
|
+
INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at)
|
|
414
|
+
VALUES (?1, 'queue.policy.publish', ?2, ?3, ?4, ?5)
|
|
415
|
+
`).bind(idempotencyKey, session.actor.id, queueId, canonicalJson(response), createdAt),
|
|
416
|
+
auditEvent(db, {
|
|
417
|
+
key: `queue-policy:${idempotencyKey}`, action: "queue.policy_published", actorType: session.actor.type, actorId: session.actor.id,
|
|
418
|
+
targetId: queueId, details: { policyVersionId: versionId, version, managementMode: policy.managementMode, rolloutMode: publicRollout(policy.rolloutMode) }, createdAt,
|
|
419
|
+
}),
|
|
420
|
+
...(runtime ? [runtime] : []),
|
|
421
|
+
]);
|
|
422
|
+
return { ...response, idempotentReplay: false };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function updateQueue(
|
|
426
|
+
db: D1Database,
|
|
427
|
+
queueId: string,
|
|
428
|
+
input: QueueUpdateInput,
|
|
429
|
+
session: OperatorSession,
|
|
430
|
+
idempotencyKey: string,
|
|
431
|
+
): Promise<Record<string, unknown>> {
|
|
432
|
+
const replay = await mutationReplay(db, idempotencyKey, "queue.update", session.actor.id);
|
|
433
|
+
if (replay) return { ...replay, idempotentReplay: true };
|
|
434
|
+
const queue = await queueRow(db, queueId);
|
|
435
|
+
if (!queue) throw new ApiError(404, "queue_not_found", "The queue does not exist.");
|
|
436
|
+
if (input.status === "archived" && queue.openReportCount > 0) {
|
|
437
|
+
throw new ApiError(409, "queue_has_open_reports", "Move or resolve all open reports before archiving this queue.");
|
|
438
|
+
}
|
|
439
|
+
if (input.status && input.status !== "active") {
|
|
440
|
+
const fallbackUse = await db.prepare(`
|
|
441
|
+
SELECT q.id FROM queue_definitions q JOIN queue_versions qv ON qv.id = q.active_version_id
|
|
442
|
+
WHERE q.status = 'active' AND qv.management_mode IN ('ai', 'hybrid')
|
|
443
|
+
AND qv.mode != 'off' AND qv.fallback_queue_id = ?1 LIMIT 1
|
|
444
|
+
`).bind(queueId).first<{ id: string }>();
|
|
445
|
+
if (fallbackUse) {
|
|
446
|
+
throw new ApiError(409, "queue_is_active_fallback", "This queue is the required human fallback for an active AI or hybrid queue.");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const createdAt = now();
|
|
450
|
+
const next = {
|
|
451
|
+
name: input.name ?? queue.name,
|
|
452
|
+
description: input.description ?? queue.description,
|
|
453
|
+
position: input.position ?? queue.position,
|
|
454
|
+
status: input.status ?? queue.status,
|
|
455
|
+
};
|
|
456
|
+
const response = { queueId, status: next.status };
|
|
457
|
+
await db.batch([
|
|
458
|
+
db.prepare(`
|
|
459
|
+
UPDATE queue_definitions
|
|
460
|
+
SET name = ?2, description = ?3, position = ?4, status = ?5,
|
|
461
|
+
enabled = CASE WHEN ?5 = 'active' THEN 1 ELSE 0 END, updated_at = ?6
|
|
462
|
+
WHERE id = ?1
|
|
463
|
+
`).bind(queueId, next.name, next.description, next.position, next.status, createdAt),
|
|
464
|
+
db.prepare(`
|
|
465
|
+
INSERT INTO configuration_mutations (idempotency_key, action, actor_id, target_id, response_json, created_at)
|
|
466
|
+
VALUES (?1, 'queue.update', ?2, ?3, ?4, ?5)
|
|
467
|
+
`).bind(idempotencyKey, session.actor.id, queueId, canonicalJson(response), createdAt),
|
|
468
|
+
auditEvent(db, {
|
|
469
|
+
key: `queue-update:${idempotencyKey}`, action: "queue.updated", actorType: session.actor.type, actorId: session.actor.id,
|
|
470
|
+
targetId: queueId, details: next, createdAt,
|
|
471
|
+
}),
|
|
472
|
+
]);
|
|
473
|
+
return { ...response, idempotentReplay: false };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function isClosedReportState(state: string): boolean {
|
|
477
|
+
return closedStates.has(state);
|
|
478
|
+
}
|