titan-agent 6.2.0 → 6.2.1
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/README.md +4 -4
- package/dist/agent/commandPost.js +3 -2
- package/dist/agent/commandPost.js.map +1 -1
- package/dist/gateway/routes/telephony.js +75 -19
- package/dist/gateway/routes/telephony.js.map +1 -1
- package/dist/skills/builtin/phone_call.js +23 -1
- package/dist/skills/builtin/phone_call.js.map +1 -1
- package/dist/telephony/phonePolicy.js +19 -3
- package/dist/telephony/phonePolicy.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/sw.js +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
2
5
|
import { Router } from "express";
|
|
3
6
|
import { loadConfig as loadTitanConfig } from "../../config/config.js";
|
|
4
7
|
import { readReceipts, writeReceipt } from "../../receipts/store.js";
|
|
@@ -7,7 +10,12 @@ import {
|
|
|
7
10
|
createDograhClientFromConfig,
|
|
8
11
|
summarizeDograhConfig
|
|
9
12
|
} from "../../telephony/dograhClient.js";
|
|
10
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
hashPhoneForApproval,
|
|
15
|
+
normalizePhoneNumber,
|
|
16
|
+
redactPhoneNumber,
|
|
17
|
+
validateOutboundCallRequest
|
|
18
|
+
} from "../../telephony/phonePolicy.js";
|
|
11
19
|
import { isTelephonyOptedOut, listTelephonyOptOuts, recordTelephonyOptOut } from "../../telephony/optOutStore.js";
|
|
12
20
|
function safeString(value, max = 160) {
|
|
13
21
|
if (typeof value !== "string") return void 0;
|
|
@@ -34,6 +42,38 @@ function metaString(meta, key) {
|
|
|
34
42
|
const value = meta?.[key];
|
|
35
43
|
return typeof value === "string" ? value : void 0;
|
|
36
44
|
}
|
|
45
|
+
function telephonyStateHome() {
|
|
46
|
+
const envHome = process.env.TITAN_HOME?.trim();
|
|
47
|
+
if (envHome) {
|
|
48
|
+
if (envHome === "~") return homedir();
|
|
49
|
+
if (envHome.startsWith("~/")) return join(homedir(), envHome.slice(2));
|
|
50
|
+
return envHome;
|
|
51
|
+
}
|
|
52
|
+
return join(homedir(), ".titan");
|
|
53
|
+
}
|
|
54
|
+
function consumedApprovalsPath() {
|
|
55
|
+
return join(telephonyStateHome(), "telephony-consumed-approvals.json");
|
|
56
|
+
}
|
|
57
|
+
function readConsumedApprovalIds() {
|
|
58
|
+
const path = consumedApprovalsPath();
|
|
59
|
+
if (!existsSync(path)) return /* @__PURE__ */ new Set();
|
|
60
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
61
|
+
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
62
|
+
return new Set(parsed.filter((value) => typeof value === "string"));
|
|
63
|
+
}
|
|
64
|
+
function isTelephonyApprovalConsumed(approvalActionId) {
|
|
65
|
+
if (readConsumedApprovalIds().has(approvalActionId)) return true;
|
|
66
|
+
return readReceipts({ kind: "approval_decision", limit: 1e3 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === "ok");
|
|
67
|
+
}
|
|
68
|
+
function consumeTelephonyApproval(approvalActionId) {
|
|
69
|
+
const path = consumedApprovalsPath();
|
|
70
|
+
const consumed = readConsumedApprovalIds();
|
|
71
|
+
if (consumed.has(approvalActionId)) return false;
|
|
72
|
+
consumed.add(approvalActionId);
|
|
73
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
74
|
+
writeFileSync(path, JSON.stringify([...consumed].sort(), null, 2), "utf8");
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
37
77
|
function verifyPriorApproval(approvalActionId, policy) {
|
|
38
78
|
if (!approvalActionId) {
|
|
39
79
|
return { ok: false, status: 403, error: "telephony_approval_required", message: "A matching approved Command Post approval is required before placing this outbound call." };
|
|
@@ -45,7 +85,7 @@ function verifyPriorApproval(approvalActionId, policy) {
|
|
|
45
85
|
if (approval.status !== "approved") {
|
|
46
86
|
return { ok: false, status: 403, error: "telephony_approval_not_approved", message: "The supplied outbound call approval has not been approved yet." };
|
|
47
87
|
}
|
|
48
|
-
const consumed =
|
|
88
|
+
const consumed = isTelephonyApprovalConsumed(approvalActionId);
|
|
49
89
|
if (consumed) {
|
|
50
90
|
return { ok: false, status: 409, error: "telephony_approval_already_used", message: "The supplied approval request was already used." };
|
|
51
91
|
}
|
|
@@ -56,6 +96,7 @@ function verifyPriorApproval(approvalActionId, policy) {
|
|
|
56
96
|
toNumberRedacted: policy.toNumberRedacted,
|
|
57
97
|
toNumberHash: policy.toNumberHash,
|
|
58
98
|
toNumberOptOutHash: policy.toNumberOptOutHash,
|
|
99
|
+
approvalIdentity: policy.approvalIdentity,
|
|
59
100
|
purpose: policy.purpose
|
|
60
101
|
};
|
|
61
102
|
for (const [key, value] of Object.entries(expected)) {
|
|
@@ -163,6 +204,7 @@ function createTelephonyRouter(deps = {}) {
|
|
|
163
204
|
const router = Router();
|
|
164
205
|
const loadConfig = deps.loadConfig ?? loadTitanConfig;
|
|
165
206
|
const createClient = deps.createClient ?? createDograhClientFromConfig;
|
|
207
|
+
const consumeApproval = deps.consumeApproval ?? consumeTelephonyApproval;
|
|
166
208
|
router.get("/dograh/status", async (_req, res) => {
|
|
167
209
|
try {
|
|
168
210
|
const config = loadConfig();
|
|
@@ -356,9 +398,13 @@ function createTelephonyRouter(deps = {}) {
|
|
|
356
398
|
if (policy.requiresApproval && !approved) {
|
|
357
399
|
const approval = createApproval({
|
|
358
400
|
type: "custom",
|
|
359
|
-
requestedBy: `phone-desk:${policy.mode}:${policy.toNumberHash?.slice(0, 12) ?? "unknown"}`,
|
|
401
|
+
requestedBy: `phone-desk:${policy.mode}:${policy.approvalIdentity?.slice(0, 16) ?? policy.toNumberHash?.slice(0, 12) ?? "unknown"}`,
|
|
360
402
|
payload: {
|
|
361
403
|
category: "telephony_outbound_call",
|
|
404
|
+
kind: "telephony_outbound_call",
|
|
405
|
+
title: policy.approvalIdentity ?? `${policy.mode}:${policy.workflowId}:${policy.toNumberHash}:${policy.purpose}`,
|
|
406
|
+
approvalIdentity: policy.approvalIdentity,
|
|
407
|
+
neverAutoApprove: true,
|
|
362
408
|
mode: policy.mode,
|
|
363
409
|
workflowId: policy.workflowId,
|
|
364
410
|
toNumberRedacted: policy.toNumberRedacted,
|
|
@@ -421,24 +467,34 @@ function createTelephonyRouter(deps = {}) {
|
|
|
421
467
|
}
|
|
422
468
|
campaignReservationId = reserveCampaignCall();
|
|
423
469
|
}
|
|
424
|
-
if (approved) {
|
|
425
|
-
writeReceipt({
|
|
426
|
-
kind: "approval_decision",
|
|
427
|
-
status: "ok",
|
|
428
|
-
parent_action_id: approvalActionId,
|
|
429
|
-
summary: `Dograh outbound call approved: ${policy.toNumberRedacted}`,
|
|
430
|
-
meta: {
|
|
431
|
-
mode: policy.mode,
|
|
432
|
-
workflowId: policy.workflowId,
|
|
433
|
-
toNumberRedacted: policy.toNumberRedacted,
|
|
434
|
-
toNumberHash: policy.toNumberHash,
|
|
435
|
-
note: approvalNote
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
const client = createClient(config.telephony);
|
|
440
470
|
let result;
|
|
441
471
|
try {
|
|
472
|
+
const client = createClient(config.telephony);
|
|
473
|
+
if (policy.requiresApproval) {
|
|
474
|
+
const consumed = !!approvalActionId && consumeApproval(approvalActionId);
|
|
475
|
+
if (!consumed) {
|
|
476
|
+
res.status(409).json({
|
|
477
|
+
ok: false,
|
|
478
|
+
error: "telephony_approval_already_used",
|
|
479
|
+
message: "The supplied approval request was already used.",
|
|
480
|
+
call: responseCall
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
writeReceipt({
|
|
485
|
+
kind: "approval_decision",
|
|
486
|
+
status: "ok",
|
|
487
|
+
parent_action_id: approvalActionId,
|
|
488
|
+
summary: `Dograh outbound call approved: ${policy.toNumberRedacted}`,
|
|
489
|
+
meta: {
|
|
490
|
+
mode: policy.mode,
|
|
491
|
+
workflowId: policy.workflowId,
|
|
492
|
+
toNumberRedacted: policy.toNumberRedacted,
|
|
493
|
+
toNumberHash: policy.toNumberHash,
|
|
494
|
+
note: approvalNote
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
442
498
|
result = await client.initiateOutboundCall({
|
|
443
499
|
workflowId: policy.workflowId,
|
|
444
500
|
phoneNumber: policy.toNumber,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/gateway/routes/telephony.ts"],"sourcesContent":["import { Router, type Request } from 'express';\nimport { loadConfig as loadTitanConfig } from '../../config/config.js';\nimport type { TitanConfig } from '../../config/schema.js';\nimport { readReceipts, writeReceipt } from '../../receipts/store.js';\nimport { createApproval, listApprovals } from '../../agent/commandPost.js';\nimport {\n createDograhClientFromConfig,\n summarizeDograhConfig,\n type DograhOutboundCallRequest,\n type DograhOutboundCallResult,\n type DograhWorkflowSummary,\n} from '../../telephony/dograhClient.js';\nimport { hashPhoneForApproval, normalizePhoneNumber, redactPhoneNumber, validateOutboundCallRequest } from '../../telephony/phonePolicy.js';\nimport { isTelephonyOptedOut, listTelephonyOptOuts, recordTelephonyOptOut } from '../../telephony/optOutStore.js';\n\ninterface DograhRouteClient {\n health: () => Promise<unknown>;\n listWorkflowSummaries: (status?: string) => Promise<DograhWorkflowSummary[]>;\n initiateOutboundCall: (call: DograhOutboundCallRequest) => Promise<DograhOutboundCallResult>;\n}\n\nexport interface TelephonyRouterDeps {\n loadConfig?: () => TitanConfig;\n createClient?: (config: TitanConfig['telephony']) => DograhRouteClient;\n}\n\nfunction safeString(value: unknown, max = 160): string | undefined {\n if (typeof value !== 'string') return undefined;\n return value.replace(/[\\r\\n\\t]+/g, ' ').slice(0, max).trim() || undefined;\n}\n\nfunction safeBool(value: unknown): boolean {\n return value === true || value === 'true';\n}\n\nfunction outboundResponseBody(policy: ReturnType<typeof validateOutboundCallRequest>) {\n return {\n toNumberRedacted: policy.toNumberRedacted,\n workflowId: policy.workflowId,\n purpose: policy.purpose,\n mode: policy.mode,\n requiresApproval: policy.requiresApproval,\n };\n}\n\nfunction safeActionId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_-]{8,80}$/.test(trimmed) ? trimmed : undefined;\n}\n\nfunction metaString(meta: Record<string, unknown> | undefined, key: string): string | undefined {\n const value = meta?.[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction verifyPriorApproval(approvalActionId: string | undefined, policy: ReturnType<typeof validateOutboundCallRequest>):\n | { ok: true }\n | { ok: false; status: number; error: string; message: string } {\n if (!approvalActionId) {\n return { ok: false, status: 403, error: 'telephony_approval_required', message: 'A matching approved Command Post approval is required before placing this outbound call.' };\n }\n const approval = listApprovals().find((item) => item.id === approvalActionId && item.type === 'custom');\n if (!approval || approval.payload?.category !== 'telephony_outbound_call') {\n return { ok: false, status: 403, error: 'telephony_approval_required', message: 'No matching Command Post approval was found for this outbound call.' };\n }\n if (approval.status !== 'approved') {\n return { ok: false, status: 403, error: 'telephony_approval_not_approved', message: 'The supplied outbound call approval has not been approved yet.' };\n }\n const consumed = readReceipts({ kind: 'approval_decision', limit: 1000 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === 'ok');\n if (consumed) {\n return { ok: false, status: 409, error: 'telephony_approval_already_used', message: 'The supplied approval request was already used.' };\n }\n const payload = approval.payload ?? {};\n const expected: Record<string, string | undefined> = {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n };\n for (const [key, value] of Object.entries(expected)) {\n const actual = payload[key];\n if (value !== undefined && actual !== value) {\n return { ok: false, status: 409, error: 'telephony_approval_mismatch', message: 'The supplied approval request does not match this outbound call.' };\n }\n }\n return { ok: true };\n}\n\nconst campaignReservations = new Map<string, number>();\n\nfunction purgeOldCampaignReservations(now = Date.now()): void {\n const cutoff = now - 60 * 60 * 1000;\n for (const [id, ts] of campaignReservations.entries()) {\n if (ts < cutoff) campaignReservations.delete(id);\n }\n}\n\nfunction campaignCallsInLastHour(now = Date.now()): number {\n purgeOldCampaignReservations(now);\n const cutoff = now - 60 * 60 * 1000;\n const completed = readReceipts({ kind: 'tool_call', limit: 1000 }).filter((receipt) => {\n if (receipt.status !== 'ok') return false;\n if (metaString(receipt.meta, 'mode') !== 'campaign') return false;\n const ts = Date.parse(receipt.ts);\n return Number.isFinite(ts) && ts >= cutoff;\n }).length;\n return completed + campaignReservations.size;\n}\n\nfunction reserveCampaignCall(): string {\n const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n campaignReservations.set(id, Date.now());\n return id;\n}\n\nfunction releaseCampaignReservation(id: string | undefined): void {\n if (id) campaignReservations.delete(id);\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number' && Number.isFinite(value)) return String(value);\n }\n return undefined;\n}\n\nfunction numberListContains(numbers: string[], candidate: string | null): boolean {\n if (!candidate) return false;\n return numbers.some((number) => normalizePhoneNumber(number) === candidate);\n}\n\nfunction hasOptOutIntent(transcript: string | undefined, keywords: string[]): string | undefined {\n if (!transcript) return undefined;\n const lower = transcript.toLowerCase();\n return keywords.find((keyword) => {\n const normalized = keyword.trim().toLowerCase();\n return normalized.length > 0 && new RegExp(`\\\\b${normalized.replace(/[.*+?^${}()|[\\\\]\\\\]/g, '\\\\$&')}\\\\b`, 'i').test(lower);\n });\n}\n\nfunction sanitizeTelephonyText(value: unknown): unknown {\n if (typeof value === 'string') {\n return value\n .replace(/\\+?\\d[\\d\\s().-]{7,}\\d/g, '[REDACTED_PHONE]')\n .replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]')\n .replace(/\\b(?:api[_-]?key|authorization|token|secret)\\b\\s*[:=]\\s*[^\\s,;]+/gi, '$1=[REDACTED]')\n .replace(/dg_[A-Za-z0-9._~+/=-]+/g, '[REDACTED_DOGRAH_KEY]')\n .slice(0, 500);\n }\n if (Array.isArray(value)) return value.slice(0, 20).map(sanitizeTelephonyText);\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value as Record<string, unknown>).slice(0, 50)) {\n if (/api[_-]?key|authorization|token|secret|password/i.test(key)) {\n out[key] = '[REDACTED]';\n } else {\n out[key] = sanitizeTelephonyText(nested);\n }\n }\n return out;\n }\n return value;\n}\n\nfunction verifyDograhWebhook(req: Request, config: TitanConfig): { ok: true } | { ok: false; status: number; error: string; message: string } {\n const expected = config.telephony.dograh.apiKey?.trim();\n if (!expected) {\n return { ok: false, status: 403, error: 'dograh_webhook_secret_missing', message: 'Dograh inbound webhooks require telephony.dograh.apiKey.' };\n }\n const auth = typeof req.headers.authorization === 'string' ? req.headers.authorization : '';\n const bearer = auth.toLowerCase().startsWith('bearer ') ? auth.slice(7).trim() : '';\n const headerToken = typeof req.headers['x-titan-telephony-token'] === 'string' ? req.headers['x-titan-telephony-token'].trim() : '';\n if (bearer !== expected && headerToken !== expected) {\n return { ok: false, status: 401, error: 'dograh_webhook_unauthorized', message: 'Dograh inbound webhook authentication failed.' };\n }\n return { ok: true };\n}\n\nfunction telephonyReceiptMeta(receipt: ReturnType<typeof readReceipts>[number]): Record<string, unknown> {\n return {\n actionId: receipt.action_id,\n ts: receipt.ts,\n kind: receipt.kind,\n status: receipt.status,\n summary: receipt.summary,\n source: typeof receipt.meta?.source === 'string' ? receipt.meta.source : undefined,\n mode: typeof receipt.meta?.mode === 'string' ? receipt.meta.mode : undefined,\n direction: typeof receipt.meta?.direction === 'string' ? receipt.meta.direction : undefined,\n toNumberRedacted: typeof receipt.meta?.toNumberRedacted === 'string' ? receipt.meta.toNumberRedacted : undefined,\n fromNumberRedacted: typeof receipt.meta?.fromNumberRedacted === 'string' ? receipt.meta.fromNumberRedacted : undefined,\n workflowId: typeof receipt.meta?.workflowId === 'string' ? receipt.meta.workflowId : undefined,\n workflowRunId: receipt.meta?.workflowRunId,\n callId: typeof receipt.meta?.callId === 'string' ? receipt.meta.callId : undefined,\n };\n}\n\nexport function createTelephonyRouter(deps: TelephonyRouterDeps = {}): Router {\n const router = Router();\n const loadConfig = deps.loadConfig ?? loadTitanConfig;\n const createClient = deps.createClient ?? createDograhClientFromConfig;\n\n router.get('/dograh/status', async (_req, res) => {\n try {\n const config = loadConfig();\n const telephony = config.telephony;\n const client = createClient(telephony);\n const health = await client.health();\n res.json({\n ok: Boolean((health as { ok?: boolean }).ok),\n provider: telephony.provider,\n config: summarizeDograhConfig(telephony),\n dograh: sanitizeTelephonyText(health),\n });\n } catch (error) {\n res.status(500).json({\n ok: false,\n error: 'dograh_status_failed',\n message: (error as Error).message,\n });\n }\n });\n\n router.get('/dograh/workflows', async (req, res) => {\n try {\n const status = typeof req.query.status === 'string' && req.query.status.trim()\n ? req.query.status.trim()\n : 'active';\n const config = loadConfig();\n const client = createClient(config.telephony);\n const workflows = await client.listWorkflowSummaries(status);\n res.json({ workflows, count: workflows.length });\n } catch (error) {\n res.status(502).json({\n error: 'dograh_workflows_unavailable',\n message: (error as Error).message,\n });\n }\n });\n\n router.get('/dograh/calls', (req, res) => {\n const limit = Number(req.query.limit ?? 100);\n const calls = readReceipts({ limit: Number.isFinite(limit) ? Math.min(Math.max(Math.floor(limit), 1), 500) : 100 })\n .filter((receipt) => receipt.meta?.source === 'dograh' || String(receipt.summary).toLowerCase().includes('dograh'))\n .map(telephonyReceiptMeta);\n res.json({ calls, count: calls.length });\n });\n\n router.get('/dograh/opt-outs', (_req, res) => {\n const optOuts = listTelephonyOptOuts().map((entry) => ({\n phoneRedacted: entry.phoneRedacted,\n reason: entry.reason,\n source: entry.source,\n recordedAt: entry.recordedAt,\n callId: entry.callId,\n }));\n res.json({ optOuts, count: optOuts.length });\n });\n\n router.post('/dograh/inbound', (req, res) => {\n try {\n const config = loadConfig();\n if (!config.telephony.enabled) {\n res.status(403).json({ ok: false, error: 'telephony_disabled', message: 'Telephony is disabled.' });\n return;\n }\n\n const webhookAuth = verifyDograhWebhook(req, config);\n if ('status' in webhookAuth) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound webhook rejected: authentication failed',\n meta: { source: 'dograh', direction: 'inbound', error: webhookAuth.error },\n });\n res.status(webhookAuth.status).json({ ok: false, error: webhookAuth.error, message: webhookAuth.message });\n return;\n }\n\n const fromNumber = normalizePhoneNumber(firstString(req.body?.fromNumber, req.body?.from, req.body?.callerNumber, req.body?.caller));\n const toNumber = normalizePhoneNumber(firstString(req.body?.toNumber, req.body?.to, req.body?.calledNumber, req.body?.called));\n const fromNumberRedacted = redactPhoneNumber(fromNumber);\n const toNumberRedacted = redactPhoneNumber(toNumber);\n const transcript = safeString(firstString(req.body?.transcript, req.body?.message, req.body?.text), 500);\n const callId = safeString(firstString(req.body?.callId, req.body?.call_id, req.body?.providerCallId, req.body?.provider_sid), 120);\n const workflowRunId = firstString(req.body?.workflowRunId, req.body?.workflow_run_id, req.body?.runId);\n\n if (!fromNumber || !toNumber) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound call blocked: invalid phone metadata',\n meta: { source: 'dograh', direction: 'inbound', fromNumberRedacted, toNumberRedacted, callId },\n });\n res.status(400).json({ ok: false, error: 'telephony_invalid_inbound_call', message: 'Inbound call requires valid E.164 caller and called numbers.' });\n return;\n }\n\n const isAdminLine = numberListContains(config.telephony.adminNumbers, toNumber);\n const isPublicLine = numberListContains(config.telephony.publicNumbers, toNumber) || !isAdminLine;\n const isAdminCaller = numberListContains(config.telephony.adminNumbers, fromNumber);\n const mode = isAdminLine ? 'admin' : 'receptionist';\n const optOutKeyword = hasOptOutIntent(transcript, config.telephony.optOutKeywords);\n let optOutRecorded = false;\n if (optOutKeyword && !isAdminLine) {\n recordTelephonyOptOut({\n phoneNumber: fromNumber,\n phoneHash: hashPhoneForApproval(config.telephony, fromNumber),\n reason: `Inbound caller said opt-out keyword: ${optOutKeyword}`,\n callId,\n });\n optOutRecorded = true;\n }\n\n if (isAdminLine && !isAdminCaller) {\n writeReceipt({\n kind: 'approval_decision',\n status: 'fail',\n summary: `Dograh admin caller rejected: ${fromNumberRedacted}`,\n meta: { source: 'dograh', direction: 'inbound', mode, fromNumberRedacted, toNumberRedacted, callId, workflowRunId },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_admin_caller_not_allowed',\n message: 'Caller is not authorized for the TITAN admin phone line.',\n mode,\n authorized: false,\n fromNumberRedacted,\n toNumberRedacted,\n });\n return;\n }\n\n writeReceipt({\n kind: 'tool_call',\n status: 'ok',\n summary: isAdminLine\n ? `Dograh admin call accepted: ${fromNumberRedacted}`\n : `Dograh receptionist call received: ${fromNumberRedacted}`,\n meta: {\n source: 'dograh',\n direction: 'inbound',\n mode,\n fromNumberRedacted,\n toNumberRedacted,\n callId,\n workflowRunId,\n optOutRecorded,\n transcriptSummary: transcript ? String(sanitizeTelephonyText(transcript)).slice(0, 120) : undefined,\n },\n });\n\n res.json({\n ok: true,\n mode: isAdminLine ? 'admin' : 'receptionist',\n authorized: isAdminLine && isAdminCaller,\n nextAction: isPublicLine && !isAdminLine ? 'record_message' : 'admin_control',\n fromNumberRedacted,\n toNumberRedacted,\n optOutRecorded,\n });\n } catch (error) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound call route failed',\n meta: { source: 'dograh', direction: 'inbound', error: (error as Error).message.slice(0, 200) },\n });\n res.status(500).json({ ok: false, error: 'dograh_inbound_route_failed', message: (error as Error).message });\n }\n });\n\n router.post('/dograh/call', async (req, res) => {\n try {\n const config = loadConfig();\n const policy = validateOutboundCallRequest(config.telephony, req.body ?? {});\n if (!policy.ok) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh outbound call blocked: ${policy.reason ?? 'policy failure'}`,\n meta: { mode: policy.mode, toNumberRedacted: policy.toNumberRedacted },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_policy_blocked',\n message: policy.reason,\n call: { toNumberRedacted: policy.toNumberRedacted, mode: policy.mode },\n });\n return;\n }\n\n const approved = safeBool(req.body?.approved);\n const approvalActionId = safeActionId(req.body?.approvalActionId);\n const approvalNote = safeString(req.body?.approvalNote, 200);\n const responseCall = outboundResponseBody(policy);\n\n if (policy.mode === 'campaign' && isTelephonyOptedOut(policy.toNumberHash, policy.toNumberOptOutHash)) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh campaign call blocked by opt-out: ${policy.toNumberRedacted}`,\n meta: { source: 'dograh', mode: policy.mode, toNumberRedacted: policy.toNumberRedacted, toNumberHash: policy.toNumberHash },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_policy_blocked',\n message: 'Destination number has opted out of campaign calls.',\n call: responseCall,\n });\n return;\n }\n\n if (policy.requiresApproval && !approved) {\n const approval = createApproval({\n type: 'custom',\n requestedBy: `phone-desk:${policy.mode}:${policy.toNumberHash?.slice(0, 12) ?? 'unknown'}`,\n payload: {\n category: 'telephony_outbound_call',\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n source: 'dograh',\n },\n });\n const receipt = writeReceipt({\n kind: 'approval_request',\n status: 'pending',\n summary: `Dograh outbound call approval required: ${policy.toNumberRedacted}`,\n meta: {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n },\n });\n res.status(202).json({\n ok: false,\n status: 'pending_approval',\n approvalActionId: approval.id,\n receiptActionId: receipt.action_id,\n call: responseCall,\n });\n return;\n }\n\n if (policy.requiresApproval) {\n const approval = verifyPriorApproval(approvalActionId, policy);\n if ('status' in approval) {\n res.status(approval.status).json({\n ok: false,\n error: approval.error,\n message: approval.message,\n call: responseCall,\n });\n return;\n }\n }\n\n let campaignReservationId: string | undefined;\n if (policy.mode === 'campaign') {\n if (campaignCallsInLastHour() >= config.telephony.maxCallsPerHour) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh campaign call rate-limited: ${policy.toNumberRedacted}`,\n meta: { mode: policy.mode, workflowId: policy.workflowId, toNumberRedacted: policy.toNumberRedacted, toNumberHash: policy.toNumberHash },\n });\n res.status(429).json({\n ok: false,\n error: 'telephony_rate_limited',\n message: `Campaign outbound call limit reached: ${config.telephony.maxCallsPerHour}/hour.`,\n call: responseCall,\n });\n return;\n }\n campaignReservationId = reserveCampaignCall();\n }\n\n if (approved) {\n writeReceipt({\n kind: 'approval_decision',\n status: 'ok',\n parent_action_id: approvalActionId,\n summary: `Dograh outbound call approved: ${policy.toNumberRedacted}`,\n meta: {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n note: approvalNote,\n },\n });\n }\n\n const client = createClient(config.telephony);\n let result: DograhOutboundCallResult;\n try {\n result = await client.initiateOutboundCall({\n workflowId: policy.workflowId!,\n phoneNumber: policy.toNumber!,\n telephonyConfigurationId: policy.telephonyConfigurationId,\n fromPhoneNumberId: policy.fromPhoneNumberId,\n });\n } finally {\n releaseCampaignReservation(campaignReservationId);\n }\n\n if (!result.ok) {\n writeReceipt({\n kind: 'tool_call',\n status: 'fail',\n summary: `Dograh outbound call failed: ${policy.toNumberRedacted}`,\n meta: { status: result.status, error: result.error, workflowId: policy.workflowId },\n });\n res.status(502).json({\n ok: false,\n status: 'failed',\n error: 'dograh_call_failed',\n message: result.error,\n call: { ...responseCall, workflowRunId: result.workflowRunId },\n });\n return;\n }\n\n writeReceipt({\n kind: 'tool_call',\n status: 'ok',\n summary: `Dograh outbound call started: ${policy.toNumberRedacted}`,\n meta: { workflowRunId: result.workflowRunId, workflowId: policy.workflowId, mode: policy.mode },\n });\n res.json({\n ok: true,\n status: 'started',\n call: { ...responseCall, workflowRunId: result.workflowRunId },\n });\n } catch (error) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh outbound call route failed',\n meta: { error: (error as Error).message.slice(0, 200) },\n });\n res.status(500).json({\n ok: false,\n error: 'dograh_call_route_failed',\n message: (error as Error).message,\n });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/gateway/routes/telephony.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { homedir } from 'os';\nimport { dirname, join } from 'path';\nimport { Router, type Request } from 'express';\nimport { loadConfig as loadTitanConfig } from '../../config/config.js';\nimport type { TitanConfig } from '../../config/schema.js';\nimport { readReceipts, writeReceipt } from '../../receipts/store.js';\nimport { createApproval, listApprovals } from '../../agent/commandPost.js';\nimport {\n createDograhClientFromConfig,\n summarizeDograhConfig,\n type DograhOutboundCallRequest,\n type DograhOutboundCallResult,\n type DograhWorkflowSummary,\n} from '../../telephony/dograhClient.js';\nimport {\n hashPhoneForApproval,\n normalizePhoneNumber,\n redactPhoneNumber,\n validateOutboundCallRequest,\n} from '../../telephony/phonePolicy.js';\nimport { isTelephonyOptedOut, listTelephonyOptOuts, recordTelephonyOptOut } from '../../telephony/optOutStore.js';\n\ninterface DograhRouteClient {\n health: () => Promise<unknown>;\n listWorkflowSummaries: (status?: string) => Promise<DograhWorkflowSummary[]>;\n initiateOutboundCall: (call: DograhOutboundCallRequest) => Promise<DograhOutboundCallResult>;\n}\n\nexport interface TelephonyRouterDeps {\n loadConfig?: () => TitanConfig;\n createClient?: (config: TitanConfig['telephony']) => DograhRouteClient;\n consumeApproval?: (approvalActionId: string) => boolean;\n}\n\nfunction safeString(value: unknown, max = 160): string | undefined {\n if (typeof value !== 'string') return undefined;\n return value.replace(/[\\r\\n\\t]+/g, ' ').slice(0, max).trim() || undefined;\n}\n\nfunction safeBool(value: unknown): boolean {\n return value === true || value === 'true';\n}\n\nfunction outboundResponseBody(policy: ReturnType<typeof validateOutboundCallRequest>) {\n return {\n toNumberRedacted: policy.toNumberRedacted,\n workflowId: policy.workflowId,\n purpose: policy.purpose,\n mode: policy.mode,\n requiresApproval: policy.requiresApproval,\n };\n}\n\nfunction safeActionId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_-]{8,80}$/.test(trimmed) ? trimmed : undefined;\n}\n\nfunction metaString(meta: Record<string, unknown> | undefined, key: string): string | undefined {\n const value = meta?.[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction telephonyStateHome(): string {\n const envHome = process.env.TITAN_HOME?.trim();\n if (envHome) {\n if (envHome === '~') return homedir();\n if (envHome.startsWith('~/')) return join(homedir(), envHome.slice(2));\n return envHome;\n }\n return join(homedir(), '.titan');\n}\n\nfunction consumedApprovalsPath(): string {\n return join(telephonyStateHome(), 'telephony-consumed-approvals.json');\n}\n\nfunction readConsumedApprovalIds(): Set<string> {\n const path = consumedApprovalsPath();\n if (!existsSync(path)) return new Set();\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n return new Set(parsed.filter((value): value is string => typeof value === 'string'));\n}\n\nfunction isTelephonyApprovalConsumed(approvalActionId: string): boolean {\n if (readConsumedApprovalIds().has(approvalActionId)) return true;\n return readReceipts({ kind: 'approval_decision', limit: 1000 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === 'ok');\n}\n\nfunction consumeTelephonyApproval(approvalActionId: string): boolean {\n const path = consumedApprovalsPath();\n const consumed = readConsumedApprovalIds();\n if (consumed.has(approvalActionId)) return false;\n consumed.add(approvalActionId);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, JSON.stringify([...consumed].sort(), null, 2), 'utf8');\n return true;\n}\n\nfunction verifyPriorApproval(approvalActionId: string | undefined, policy: ReturnType<typeof validateOutboundCallRequest>):\n | { ok: true }\n | { ok: false; status: number; error: string; message: string } {\n if (!approvalActionId) {\n return { ok: false, status: 403, error: 'telephony_approval_required', message: 'A matching approved Command Post approval is required before placing this outbound call.' };\n }\n const approval = listApprovals().find((item) => item.id === approvalActionId && item.type === 'custom');\n if (!approval || approval.payload?.category !== 'telephony_outbound_call') {\n return { ok: false, status: 403, error: 'telephony_approval_required', message: 'No matching Command Post approval was found for this outbound call.' };\n }\n if (approval.status !== 'approved') {\n return { ok: false, status: 403, error: 'telephony_approval_not_approved', message: 'The supplied outbound call approval has not been approved yet.' };\n }\n const consumed = isTelephonyApprovalConsumed(approvalActionId);\n if (consumed) {\n return { ok: false, status: 409, error: 'telephony_approval_already_used', message: 'The supplied approval request was already used.' };\n }\n const payload = approval.payload ?? {};\n const expected: Record<string, string | undefined> = {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n approvalIdentity: policy.approvalIdentity,\n purpose: policy.purpose,\n };\n for (const [key, value] of Object.entries(expected)) {\n const actual = payload[key];\n if (value !== undefined && actual !== value) {\n return { ok: false, status: 409, error: 'telephony_approval_mismatch', message: 'The supplied approval request does not match this outbound call.' };\n }\n }\n return { ok: true };\n}\n\nconst campaignReservations = new Map<string, number>();\n\nfunction purgeOldCampaignReservations(now = Date.now()): void {\n const cutoff = now - 60 * 60 * 1000;\n for (const [id, ts] of campaignReservations.entries()) {\n if (ts < cutoff) campaignReservations.delete(id);\n }\n}\n\nfunction campaignCallsInLastHour(now = Date.now()): number {\n purgeOldCampaignReservations(now);\n const cutoff = now - 60 * 60 * 1000;\n const completed = readReceipts({ kind: 'tool_call', limit: 1000 }).filter((receipt) => {\n if (receipt.status !== 'ok') return false;\n if (metaString(receipt.meta, 'mode') !== 'campaign') return false;\n const ts = Date.parse(receipt.ts);\n return Number.isFinite(ts) && ts >= cutoff;\n }).length;\n return completed + campaignReservations.size;\n}\n\nfunction reserveCampaignCall(): string {\n const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n campaignReservations.set(id, Date.now());\n return id;\n}\n\nfunction releaseCampaignReservation(id: string | undefined): void {\n if (id) campaignReservations.delete(id);\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number' && Number.isFinite(value)) return String(value);\n }\n return undefined;\n}\n\nfunction numberListContains(numbers: string[], candidate: string | null): boolean {\n if (!candidate) return false;\n return numbers.some((number) => normalizePhoneNumber(number) === candidate);\n}\n\nfunction hasOptOutIntent(transcript: string | undefined, keywords: string[]): string | undefined {\n if (!transcript) return undefined;\n const lower = transcript.toLowerCase();\n return keywords.find((keyword) => {\n const normalized = keyword.trim().toLowerCase();\n return normalized.length > 0 && new RegExp(`\\\\b${normalized.replace(/[.*+?^${}()|[\\\\]\\\\]/g, '\\\\$&')}\\\\b`, 'i').test(lower);\n });\n}\n\nfunction sanitizeTelephonyText(value: unknown): unknown {\n if (typeof value === 'string') {\n return value\n .replace(/\\+?\\d[\\d\\s().-]{7,}\\d/g, '[REDACTED_PHONE]')\n .replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]')\n .replace(/\\b(?:api[_-]?key|authorization|token|secret)\\b\\s*[:=]\\s*[^\\s,;]+/gi, '$1=[REDACTED]')\n .replace(/dg_[A-Za-z0-9._~+/=-]+/g, '[REDACTED_DOGRAH_KEY]')\n .slice(0, 500);\n }\n if (Array.isArray(value)) return value.slice(0, 20).map(sanitizeTelephonyText);\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value as Record<string, unknown>).slice(0, 50)) {\n if (/api[_-]?key|authorization|token|secret|password/i.test(key)) {\n out[key] = '[REDACTED]';\n } else {\n out[key] = sanitizeTelephonyText(nested);\n }\n }\n return out;\n }\n return value;\n}\n\nfunction verifyDograhWebhook(req: Request, config: TitanConfig): { ok: true } | { ok: false; status: number; error: string; message: string } {\n const expected = config.telephony.dograh.apiKey?.trim();\n if (!expected) {\n return { ok: false, status: 403, error: 'dograh_webhook_secret_missing', message: 'Dograh inbound webhooks require telephony.dograh.apiKey.' };\n }\n const auth = typeof req.headers.authorization === 'string' ? req.headers.authorization : '';\n const bearer = auth.toLowerCase().startsWith('bearer ') ? auth.slice(7).trim() : '';\n const headerToken = typeof req.headers['x-titan-telephony-token'] === 'string' ? req.headers['x-titan-telephony-token'].trim() : '';\n if (bearer !== expected && headerToken !== expected) {\n return { ok: false, status: 401, error: 'dograh_webhook_unauthorized', message: 'Dograh inbound webhook authentication failed.' };\n }\n return { ok: true };\n}\n\nfunction telephonyReceiptMeta(receipt: ReturnType<typeof readReceipts>[number]): Record<string, unknown> {\n return {\n actionId: receipt.action_id,\n ts: receipt.ts,\n kind: receipt.kind,\n status: receipt.status,\n summary: receipt.summary,\n source: typeof receipt.meta?.source === 'string' ? receipt.meta.source : undefined,\n mode: typeof receipt.meta?.mode === 'string' ? receipt.meta.mode : undefined,\n direction: typeof receipt.meta?.direction === 'string' ? receipt.meta.direction : undefined,\n toNumberRedacted: typeof receipt.meta?.toNumberRedacted === 'string' ? receipt.meta.toNumberRedacted : undefined,\n fromNumberRedacted: typeof receipt.meta?.fromNumberRedacted === 'string' ? receipt.meta.fromNumberRedacted : undefined,\n workflowId: typeof receipt.meta?.workflowId === 'string' ? receipt.meta.workflowId : undefined,\n workflowRunId: receipt.meta?.workflowRunId,\n callId: typeof receipt.meta?.callId === 'string' ? receipt.meta.callId : undefined,\n };\n}\n\nexport function createTelephonyRouter(deps: TelephonyRouterDeps = {}): Router {\n const router = Router();\n const loadConfig = deps.loadConfig ?? loadTitanConfig;\n const createClient = deps.createClient ?? createDograhClientFromConfig;\n const consumeApproval = deps.consumeApproval ?? consumeTelephonyApproval;\n\n router.get('/dograh/status', async (_req, res) => {\n try {\n const config = loadConfig();\n const telephony = config.telephony;\n const client = createClient(telephony);\n const health = await client.health();\n res.json({\n ok: Boolean((health as { ok?: boolean }).ok),\n provider: telephony.provider,\n config: summarizeDograhConfig(telephony),\n dograh: sanitizeTelephonyText(health),\n });\n } catch (error) {\n res.status(500).json({\n ok: false,\n error: 'dograh_status_failed',\n message: (error as Error).message,\n });\n }\n });\n\n router.get('/dograh/workflows', async (req, res) => {\n try {\n const status = typeof req.query.status === 'string' && req.query.status.trim()\n ? req.query.status.trim()\n : 'active';\n const config = loadConfig();\n const client = createClient(config.telephony);\n const workflows = await client.listWorkflowSummaries(status);\n res.json({ workflows, count: workflows.length });\n } catch (error) {\n res.status(502).json({\n error: 'dograh_workflows_unavailable',\n message: (error as Error).message,\n });\n }\n });\n\n router.get('/dograh/calls', (req, res) => {\n const limit = Number(req.query.limit ?? 100);\n const calls = readReceipts({ limit: Number.isFinite(limit) ? Math.min(Math.max(Math.floor(limit), 1), 500) : 100 })\n .filter((receipt) => receipt.meta?.source === 'dograh' || String(receipt.summary).toLowerCase().includes('dograh'))\n .map(telephonyReceiptMeta);\n res.json({ calls, count: calls.length });\n });\n\n router.get('/dograh/opt-outs', (_req, res) => {\n const optOuts = listTelephonyOptOuts().map((entry) => ({\n phoneRedacted: entry.phoneRedacted,\n reason: entry.reason,\n source: entry.source,\n recordedAt: entry.recordedAt,\n callId: entry.callId,\n }));\n res.json({ optOuts, count: optOuts.length });\n });\n\n router.post('/dograh/inbound', (req, res) => {\n try {\n const config = loadConfig();\n if (!config.telephony.enabled) {\n res.status(403).json({ ok: false, error: 'telephony_disabled', message: 'Telephony is disabled.' });\n return;\n }\n\n const webhookAuth = verifyDograhWebhook(req, config);\n if ('status' in webhookAuth) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound webhook rejected: authentication failed',\n meta: { source: 'dograh', direction: 'inbound', error: webhookAuth.error },\n });\n res.status(webhookAuth.status).json({ ok: false, error: webhookAuth.error, message: webhookAuth.message });\n return;\n }\n\n const fromNumber = normalizePhoneNumber(firstString(req.body?.fromNumber, req.body?.from, req.body?.callerNumber, req.body?.caller));\n const toNumber = normalizePhoneNumber(firstString(req.body?.toNumber, req.body?.to, req.body?.calledNumber, req.body?.called));\n const fromNumberRedacted = redactPhoneNumber(fromNumber);\n const toNumberRedacted = redactPhoneNumber(toNumber);\n const transcript = safeString(firstString(req.body?.transcript, req.body?.message, req.body?.text), 500);\n const callId = safeString(firstString(req.body?.callId, req.body?.call_id, req.body?.providerCallId, req.body?.provider_sid), 120);\n const workflowRunId = firstString(req.body?.workflowRunId, req.body?.workflow_run_id, req.body?.runId);\n\n if (!fromNumber || !toNumber) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound call blocked: invalid phone metadata',\n meta: { source: 'dograh', direction: 'inbound', fromNumberRedacted, toNumberRedacted, callId },\n });\n res.status(400).json({ ok: false, error: 'telephony_invalid_inbound_call', message: 'Inbound call requires valid E.164 caller and called numbers.' });\n return;\n }\n\n const isAdminLine = numberListContains(config.telephony.adminNumbers, toNumber);\n const isPublicLine = numberListContains(config.telephony.publicNumbers, toNumber) || !isAdminLine;\n const isAdminCaller = numberListContains(config.telephony.adminNumbers, fromNumber);\n const mode = isAdminLine ? 'admin' : 'receptionist';\n const optOutKeyword = hasOptOutIntent(transcript, config.telephony.optOutKeywords);\n let optOutRecorded = false;\n if (optOutKeyword && !isAdminLine) {\n recordTelephonyOptOut({\n phoneNumber: fromNumber,\n phoneHash: hashPhoneForApproval(config.telephony, fromNumber),\n reason: `Inbound caller said opt-out keyword: ${optOutKeyword}`,\n callId,\n });\n optOutRecorded = true;\n }\n\n if (isAdminLine && !isAdminCaller) {\n writeReceipt({\n kind: 'approval_decision',\n status: 'fail',\n summary: `Dograh admin caller rejected: ${fromNumberRedacted}`,\n meta: { source: 'dograh', direction: 'inbound', mode, fromNumberRedacted, toNumberRedacted, callId, workflowRunId },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_admin_caller_not_allowed',\n message: 'Caller is not authorized for the TITAN admin phone line.',\n mode,\n authorized: false,\n fromNumberRedacted,\n toNumberRedacted,\n });\n return;\n }\n\n writeReceipt({\n kind: 'tool_call',\n status: 'ok',\n summary: isAdminLine\n ? `Dograh admin call accepted: ${fromNumberRedacted}`\n : `Dograh receptionist call received: ${fromNumberRedacted}`,\n meta: {\n source: 'dograh',\n direction: 'inbound',\n mode,\n fromNumberRedacted,\n toNumberRedacted,\n callId,\n workflowRunId,\n optOutRecorded,\n transcriptSummary: transcript ? String(sanitizeTelephonyText(transcript)).slice(0, 120) : undefined,\n },\n });\n\n res.json({\n ok: true,\n mode: isAdminLine ? 'admin' : 'receptionist',\n authorized: isAdminLine && isAdminCaller,\n nextAction: isPublicLine && !isAdminLine ? 'record_message' : 'admin_control',\n fromNumberRedacted,\n toNumberRedacted,\n optOutRecorded,\n });\n } catch (error) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound call route failed',\n meta: { source: 'dograh', direction: 'inbound', error: (error as Error).message.slice(0, 200) },\n });\n res.status(500).json({ ok: false, error: 'dograh_inbound_route_failed', message: (error as Error).message });\n }\n });\n\n router.post('/dograh/call', async (req, res) => {\n try {\n const config = loadConfig();\n const policy = validateOutboundCallRequest(config.telephony, req.body ?? {});\n if (!policy.ok) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh outbound call blocked: ${policy.reason ?? 'policy failure'}`,\n meta: { mode: policy.mode, toNumberRedacted: policy.toNumberRedacted },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_policy_blocked',\n message: policy.reason,\n call: { toNumberRedacted: policy.toNumberRedacted, mode: policy.mode },\n });\n return;\n }\n\n const approved = safeBool(req.body?.approved);\n const approvalActionId = safeActionId(req.body?.approvalActionId);\n const approvalNote = safeString(req.body?.approvalNote, 200);\n const responseCall = outboundResponseBody(policy);\n\n if (policy.mode === 'campaign' && isTelephonyOptedOut(policy.toNumberHash, policy.toNumberOptOutHash)) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh campaign call blocked by opt-out: ${policy.toNumberRedacted}`,\n meta: { source: 'dograh', mode: policy.mode, toNumberRedacted: policy.toNumberRedacted, toNumberHash: policy.toNumberHash },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_policy_blocked',\n message: 'Destination number has opted out of campaign calls.',\n call: responseCall,\n });\n return;\n }\n\n if (policy.requiresApproval && !approved) {\n const approval = createApproval({\n type: 'custom',\n requestedBy: `phone-desk:${policy.mode}:${policy.approvalIdentity?.slice(0, 16) ?? policy.toNumberHash?.slice(0, 12) ?? 'unknown'}`,\n payload: {\n category: 'telephony_outbound_call',\n kind: 'telephony_outbound_call',\n title: policy.approvalIdentity ?? `${policy.mode}:${policy.workflowId}:${policy.toNumberHash}:${policy.purpose}`,\n approvalIdentity: policy.approvalIdentity,\n neverAutoApprove: true,\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n source: 'dograh',\n },\n });\n const receipt = writeReceipt({\n kind: 'approval_request',\n status: 'pending',\n summary: `Dograh outbound call approval required: ${policy.toNumberRedacted}`,\n meta: {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n },\n });\n res.status(202).json({\n ok: false,\n status: 'pending_approval',\n approvalActionId: approval.id,\n receiptActionId: receipt.action_id,\n call: responseCall,\n });\n return;\n }\n\n if (policy.requiresApproval) {\n const approval = verifyPriorApproval(approvalActionId, policy);\n if ('status' in approval) {\n res.status(approval.status).json({\n ok: false,\n error: approval.error,\n message: approval.message,\n call: responseCall,\n });\n return;\n }\n }\n\n let campaignReservationId: string | undefined;\n if (policy.mode === 'campaign') {\n if (campaignCallsInLastHour() >= config.telephony.maxCallsPerHour) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh campaign call rate-limited: ${policy.toNumberRedacted}`,\n meta: { mode: policy.mode, workflowId: policy.workflowId, toNumberRedacted: policy.toNumberRedacted, toNumberHash: policy.toNumberHash },\n });\n res.status(429).json({\n ok: false,\n error: 'telephony_rate_limited',\n message: `Campaign outbound call limit reached: ${config.telephony.maxCallsPerHour}/hour.`,\n call: responseCall,\n });\n return;\n }\n campaignReservationId = reserveCampaignCall();\n }\n\n let result: DograhOutboundCallResult;\n try {\n const client = createClient(config.telephony);\n if (policy.requiresApproval) {\n const consumed = !!approvalActionId && consumeApproval(approvalActionId);\n if (!consumed) {\n res.status(409).json({\n ok: false,\n error: 'telephony_approval_already_used',\n message: 'The supplied approval request was already used.',\n call: responseCall,\n });\n return;\n }\n writeReceipt({\n kind: 'approval_decision',\n status: 'ok',\n parent_action_id: approvalActionId,\n summary: `Dograh outbound call approved: ${policy.toNumberRedacted}`,\n meta: {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n note: approvalNote,\n },\n });\n }\n\n result = await client.initiateOutboundCall({\n workflowId: policy.workflowId!,\n phoneNumber: policy.toNumber!,\n telephonyConfigurationId: policy.telephonyConfigurationId,\n fromPhoneNumberId: policy.fromPhoneNumberId,\n });\n } finally {\n releaseCampaignReservation(campaignReservationId);\n }\n\n if (!result.ok) {\n writeReceipt({\n kind: 'tool_call',\n status: 'fail',\n summary: `Dograh outbound call failed: ${policy.toNumberRedacted}`,\n meta: { status: result.status, error: result.error, workflowId: policy.workflowId },\n });\n res.status(502).json({\n ok: false,\n status: 'failed',\n error: 'dograh_call_failed',\n message: result.error,\n call: { ...responseCall, workflowRunId: result.workflowRunId },\n });\n return;\n }\n\n writeReceipt({\n kind: 'tool_call',\n status: 'ok',\n summary: `Dograh outbound call started: ${policy.toNumberRedacted}`,\n meta: { workflowRunId: result.workflowRunId, workflowId: policy.workflowId, mode: policy.mode },\n });\n res.json({\n ok: true,\n status: 'started',\n call: { ...responseCall, workflowRunId: result.workflowRunId },\n });\n } catch (error) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh outbound call route failed',\n meta: { error: (error as Error).message.slice(0, 200) },\n });\n res.status(500).json({\n ok: false,\n error: 'dograh_call_route_failed',\n message: (error as Error).message,\n });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createApproval } from "../../agent/commandPost.js";
|
|
2
3
|
import { loadConfig } from "../../config/config.js";
|
|
3
4
|
import { writeReceipt } from "../../receipts/store.js";
|
|
4
5
|
import { validateOutboundCallRequest } from "../../telephony/phonePolicy.js";
|
|
@@ -37,15 +38,35 @@ function registerPhoneCallSkill() {
|
|
|
37
38
|
});
|
|
38
39
|
return `Phone call blocked: ${policy.reason ?? "policy failure"}${policy.toNumberRedacted ? ` (${policy.toNumberRedacted})` : ""}`;
|
|
39
40
|
}
|
|
41
|
+
const approval = createApproval({
|
|
42
|
+
type: "custom",
|
|
43
|
+
requestedBy: `phone-call-tool:${policy.mode}:${policy.approvalIdentity?.slice(0, 16) ?? policy.toNumberHash?.slice(0, 12) ?? "unknown"}`,
|
|
44
|
+
payload: {
|
|
45
|
+
category: "telephony_outbound_call",
|
|
46
|
+
kind: "telephony_outbound_call",
|
|
47
|
+
title: policy.approvalIdentity ?? `${policy.mode}:${policy.workflowId}:${policy.toNumberHash}:${policy.purpose}`,
|
|
48
|
+
approvalIdentity: policy.approvalIdentity,
|
|
49
|
+
neverAutoApprove: true,
|
|
50
|
+
mode: policy.mode,
|
|
51
|
+
workflowId: policy.workflowId,
|
|
52
|
+
toNumberRedacted: policy.toNumberRedacted,
|
|
53
|
+
toNumberHash: policy.toNumberHash,
|
|
54
|
+
toNumberOptOutHash: policy.toNumberOptOutHash,
|
|
55
|
+
purpose: policy.purpose,
|
|
56
|
+
source: "dograh"
|
|
57
|
+
}
|
|
58
|
+
});
|
|
40
59
|
const receipt = writeReceipt({
|
|
41
60
|
kind: "approval_request",
|
|
42
61
|
status: "pending",
|
|
43
62
|
summary: `Phone call approval required: ${policy.toNumberRedacted}`,
|
|
44
63
|
meta: {
|
|
64
|
+
approvalActionId: approval.id,
|
|
45
65
|
mode: policy.mode,
|
|
46
66
|
workflowId: policy.workflowId,
|
|
47
67
|
toNumberRedacted: policy.toNumberRedacted,
|
|
48
68
|
toNumberHash: policy.toNumberHash,
|
|
69
|
+
toNumberOptOutHash: policy.toNumberOptOutHash,
|
|
49
70
|
purpose: policy.purpose
|
|
50
71
|
}
|
|
51
72
|
});
|
|
@@ -53,7 +74,8 @@ function registerPhoneCallSkill() {
|
|
|
53
74
|
`Phone call approval required for ${policy.toNumberRedacted}.`,
|
|
54
75
|
`Purpose: ${policy.purpose}`,
|
|
55
76
|
`Mode: ${policy.mode}`,
|
|
56
|
-
`Approval action: ${
|
|
77
|
+
`Approval action: ${approval.id}`,
|
|
78
|
+
`Receipt action: ${receipt.action_id}`,
|
|
57
79
|
"No call was started. Use Phone Desk approval to place the Dograh call."
|
|
58
80
|
].join("\n");
|
|
59
81
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/phone_call.ts"],"sourcesContent":["import { loadConfig } from '../../config/config.js';\nimport { writeReceipt } from '../../receipts/store.js';\nimport { validateOutboundCallRequest } from '../../telephony/phonePolicy.js';\nimport { registerSkill } from '../registry.js';\n\nexport function registerPhoneCallSkill(): void {\n registerSkill(\n {\n name: 'phone_call',\n description: 'Request an approval-gated Dograh outbound phone call. The tool never dials directly.',\n version: '0.1.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'phone_call',\n description: 'Request a Dograh-powered outbound phone call through TITAN Phone Desk. This tool creates an approval request only; it does not directly dial. Use only when the user explicitly asks TITAN to call a person or business. Cold-call/campaign mode is blocked unless configured separately.',\n parameters: {\n type: 'object',\n properties: {\n toNumber: { type: 'string', description: 'Destination phone number in E.164 format, e.g. +15551234567' },\n purpose: { type: 'string', description: 'Human-readable reason/objective for the call' },\n workflowId: { type: 'string', description: 'Optional Dograh outbound workflow ID. Defaults to telephony.dograh.defaultOutboundWorkflowId.' },\n mode: { type: 'string', enum: ['test', 'callback', 'campaign'], description: 'Call mode. campaign remains policy-gated.' },\n },\n required: ['toNumber', 'purpose'],\n },\n execute: async (args) => {\n const config = loadConfig();\n const policy = validateOutboundCallRequest(config.telephony, args);\n if (!policy.ok) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Phone call blocked: ${policy.reason ?? 'policy failure'}`,\n meta: { mode: policy.mode, toNumberRedacted: policy.toNumberRedacted },\n });\n return `Phone call blocked: ${policy.reason ?? 'policy failure'}${policy.toNumberRedacted ? ` (${policy.toNumberRedacted})` : ''}`;\n }\n\n const receipt = writeReceipt({\n kind: 'approval_request',\n status: 'pending',\n summary: `Phone call approval required: ${policy.toNumberRedacted}`,\n meta: {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n purpose: policy.purpose,\n },\n });\n\n return [\n `Phone call approval required for ${policy.toNumberRedacted}.`,\n `Purpose: ${policy.purpose}`,\n `Mode: ${policy.mode}`,\n `Approval action: ${receipt.action_id}`,\n 'No call was started. Use Phone Desk approval to place the Dograh call.',\n ].join('\\n');\n },\n },\n );\n}\n"],"mappings":";AAAA,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,mCAAmC;AAC5C,SAAS,qBAAqB;AAEvB,SAAS,yBAA+B;AAC7C;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,YAAY;AAAA,QACV,MAAM;AAAA,QACN,YAAY;AAAA,UACV,UAAU,EAAE,MAAM,UAAU,aAAa,8DAA8D;AAAA,UACvG,SAAS,EAAE,MAAM,UAAU,aAAa,+CAA+C;AAAA,UACvF,YAAY,EAAE,MAAM,UAAU,aAAa,gGAAgG;AAAA,UAC3I,MAAM,EAAE,MAAM,UAAU,MAAM,CAAC,QAAQ,YAAY,UAAU,GAAG,aAAa,4CAA4C;AAAA,QAC3H;AAAA,QACA,UAAU,CAAC,YAAY,SAAS;AAAA,MAClC;AAAA,MACA,SAAS,OAAO,SAAS;AACvB,cAAM,SAAS,WAAW;AAC1B,cAAM,SAAS,4BAA4B,OAAO,WAAW,IAAI;AACjE,YAAI,CAAC,OAAO,IAAI;AACd,uBAAa;AAAA,YACX,MAAM;AAAA,YACN,QAAQ;AAAA,YACR,SAAS,uBAAuB,OAAO,UAAU,gBAAgB;AAAA,YACjE,MAAM,EAAE,MAAM,OAAO,MAAM,kBAAkB,OAAO,iBAAiB;AAAA,UACvE,CAAC;AACD,iBAAO,uBAAuB,OAAO,UAAU,gBAAgB,GAAG,OAAO,mBAAmB,KAAK,OAAO,gBAAgB,MAAM,EAAE;AAAA,QAClI;AAEA,cAAM,UAAU,aAAa;AAAA,UAC3B,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS,iCAAiC,OAAO,gBAAgB;AAAA,UACjE,MAAM;AAAA,YACJ,MAAM,OAAO;AAAA,YACb,YAAY,OAAO;AAAA,YACnB,kBAAkB,OAAO;AAAA,YACzB,cAAc,OAAO;AAAA,YACrB,SAAS,OAAO;AAAA,UAClB;AAAA,QACF,CAAC;AAED,eAAO;AAAA,UACL,oCAAoC,OAAO,gBAAgB;AAAA,UAC3D,YAAY,OAAO,OAAO;AAAA,UAC1B,SAAS,OAAO,IAAI;AAAA,UACpB,oBAAoB,QAAQ,SAAS;AAAA,
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/phone_call.ts"],"sourcesContent":["import { createApproval } from '../../agent/commandPost.js';\nimport { loadConfig } from '../../config/config.js';\nimport { writeReceipt } from '../../receipts/store.js';\nimport { validateOutboundCallRequest } from '../../telephony/phonePolicy.js';\nimport { registerSkill } from '../registry.js';\n\nexport function registerPhoneCallSkill(): void {\n registerSkill(\n {\n name: 'phone_call',\n description: 'Request an approval-gated Dograh outbound phone call. The tool never dials directly.',\n version: '0.1.0',\n source: 'bundled',\n enabled: true,\n },\n {\n name: 'phone_call',\n description: 'Request a Dograh-powered outbound phone call through TITAN Phone Desk. This tool creates an approval request only; it does not directly dial. Use only when the user explicitly asks TITAN to call a person or business. Cold-call/campaign mode is blocked unless configured separately.',\n parameters: {\n type: 'object',\n properties: {\n toNumber: { type: 'string', description: 'Destination phone number in E.164 format, e.g. +15551234567' },\n purpose: { type: 'string', description: 'Human-readable reason/objective for the call' },\n workflowId: { type: 'string', description: 'Optional Dograh outbound workflow ID. Defaults to telephony.dograh.defaultOutboundWorkflowId.' },\n mode: { type: 'string', enum: ['test', 'callback', 'campaign'], description: 'Call mode. campaign remains policy-gated.' },\n },\n required: ['toNumber', 'purpose'],\n },\n execute: async (args) => {\n const config = loadConfig();\n const policy = validateOutboundCallRequest(config.telephony, args);\n if (!policy.ok) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Phone call blocked: ${policy.reason ?? 'policy failure'}`,\n meta: { mode: policy.mode, toNumberRedacted: policy.toNumberRedacted },\n });\n return `Phone call blocked: ${policy.reason ?? 'policy failure'}${policy.toNumberRedacted ? ` (${policy.toNumberRedacted})` : ''}`;\n }\n\n const approval = createApproval({\n type: 'custom',\n requestedBy: `phone-call-tool:${policy.mode}:${policy.approvalIdentity?.slice(0, 16) ?? policy.toNumberHash?.slice(0, 12) ?? 'unknown'}`,\n payload: {\n category: 'telephony_outbound_call',\n kind: 'telephony_outbound_call',\n title: policy.approvalIdentity ?? `${policy.mode}:${policy.workflowId}:${policy.toNumberHash}:${policy.purpose}`,\n approvalIdentity: policy.approvalIdentity,\n neverAutoApprove: true,\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n source: 'dograh',\n },\n });\n const receipt = writeReceipt({\n kind: 'approval_request',\n status: 'pending',\n summary: `Phone call approval required: ${policy.toNumberRedacted}`,\n meta: {\n approvalActionId: approval.id,\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n },\n });\n\n return [\n `Phone call approval required for ${policy.toNumberRedacted}.`,\n `Purpose: ${policy.purpose}`,\n `Mode: ${policy.mode}`,\n `Approval action: ${approval.id}`,\n `Receipt action: ${receipt.action_id}`,\n 'No call was started. Use Phone Desk approval to place the Dograh call.',\n ].join('\\n');\n },\n },\n );\n}\n"],"mappings":";AAAA,SAAS,sBAAsB;AAC/B,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,mCAAmC;AAC5C,SAAS,qBAAqB;AAEvB,SAAS,yBAA+B;AAC7C;AAAA,IACE;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,SAAS;AAAA,MACT,QAAQ;AAAA,MACR,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,aAAa;AAAA,MACb,YAAY;AAAA,QACV,MAAM;AAAA,QACN,YAAY;AAAA,UACV,UAAU,EAAE,MAAM,UAAU,aAAa,8DAA8D;AAAA,UACvG,SAAS,EAAE,MAAM,UAAU,aAAa,+CAA+C;AAAA,UACvF,YAAY,EAAE,MAAM,UAAU,aAAa,gGAAgG;AAAA,UAC3I,MAAM,EAAE,MAAM,UAAU,MAAM,CAAC,QAAQ,YAAY,UAAU,GAAG,aAAa,4CAA4C;AAAA,QAC3H;AAAA,QACA,UAAU,CAAC,YAAY,SAAS;AAAA,MAClC;AAAA,MACA,SAAS,OAAO,SAAS;AACvB,cAAM,SAAS,WAAW;AAC1B,cAAM,SAAS,4BAA4B,OAAO,WAAW,IAAI;AACjE,YAAI,CAAC,OAAO,IAAI;AACd,uBAAa;AAAA,YACX,MAAM;AAAA,YACN,QAAQ;AAAA,YACR,SAAS,uBAAuB,OAAO,UAAU,gBAAgB;AAAA,YACjE,MAAM,EAAE,MAAM,OAAO,MAAM,kBAAkB,OAAO,iBAAiB;AAAA,UACvE,CAAC;AACD,iBAAO,uBAAuB,OAAO,UAAU,gBAAgB,GAAG,OAAO,mBAAmB,KAAK,OAAO,gBAAgB,MAAM,EAAE;AAAA,QAClI;AAEA,cAAM,WAAW,eAAe;AAAA,UAC9B,MAAM;AAAA,UACN,aAAa,mBAAmB,OAAO,IAAI,IAAI,OAAO,kBAAkB,MAAM,GAAG,EAAE,KAAK,OAAO,cAAc,MAAM,GAAG,EAAE,KAAK,SAAS;AAAA,UACtI,SAAS;AAAA,YACP,UAAU;AAAA,YACV,MAAM;AAAA,YACN,OAAO,OAAO,oBAAoB,GAAG,OAAO,IAAI,IAAI,OAAO,UAAU,IAAI,OAAO,YAAY,IAAI,OAAO,OAAO;AAAA,YAC9G,kBAAkB,OAAO;AAAA,YACzB,kBAAkB;AAAA,YAClB,MAAM,OAAO;AAAA,YACb,YAAY,OAAO;AAAA,YACnB,kBAAkB,OAAO;AAAA,YACzB,cAAc,OAAO;AAAA,YACrB,oBAAoB,OAAO;AAAA,YAC3B,SAAS,OAAO;AAAA,YAChB,QAAQ;AAAA,UACV;AAAA,QACF,CAAC;AACD,cAAM,UAAU,aAAa;AAAA,UAC3B,MAAM;AAAA,UACN,QAAQ;AAAA,UACR,SAAS,iCAAiC,OAAO,gBAAgB;AAAA,UACjE,MAAM;AAAA,YACJ,kBAAkB,SAAS;AAAA,YAC3B,MAAM,OAAO;AAAA,YACb,YAAY,OAAO;AAAA,YACnB,kBAAkB,OAAO;AAAA,YACzB,cAAc,OAAO;AAAA,YACrB,oBAAoB,OAAO;AAAA,YAC3B,SAAS,OAAO;AAAA,UAClB;AAAA,QACF,CAAC;AAED,eAAO;AAAA,UACL,oCAAoC,OAAO,gBAAgB;AAAA,UAC3D,YAAY,OAAO,OAAO;AAAA,UAC1B,SAAS,OAAO,IAAI;AAAA,UACpB,oBAAoB,SAAS,EAAE;AAAA,UAC/B,mBAAmB,QAAQ,SAAS;AAAA,UACpC;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -24,6 +24,14 @@ function hashPhoneForApproval(config, normalizedPhoneNumber) {
|
|
|
24
24
|
function hashPhoneForOptOut(normalizedPhoneNumber) {
|
|
25
25
|
return createHmac("sha256", "titan-telephony-opt-out-v1").update(normalizedPhoneNumber).digest("hex");
|
|
26
26
|
}
|
|
27
|
+
function buildTelephonyApprovalIdentity(fields) {
|
|
28
|
+
return createHmac("sha256", "titan-telephony-approval-identity-v1").update(JSON.stringify({
|
|
29
|
+
mode: fields.mode,
|
|
30
|
+
workflowId: fields.workflowId,
|
|
31
|
+
purpose: fields.purpose,
|
|
32
|
+
toNumberHash: fields.toNumberHash
|
|
33
|
+
})).digest("hex");
|
|
34
|
+
}
|
|
27
35
|
function normalizeMode(value) {
|
|
28
36
|
return value === "campaign" || value === "callback" || value === "test" ? value : "callback";
|
|
29
37
|
}
|
|
@@ -96,21 +104,29 @@ function validateOutboundCallRequest(config, input) {
|
|
|
96
104
|
};
|
|
97
105
|
}
|
|
98
106
|
const purpose = safePurpose(input.purpose) || "Outbound Dograh call";
|
|
107
|
+
const toNumberHash = hashPhoneForApproval(config, toNumber);
|
|
108
|
+
const toNumberOptOutHash = hashPhoneForOptOut(toNumber);
|
|
109
|
+
const approvalIdentity = buildTelephonyApprovalIdentity({ mode, workflowId, purpose, toNumberHash });
|
|
99
110
|
return {
|
|
100
111
|
ok: true,
|
|
101
112
|
toNumber,
|
|
102
113
|
toNumberRedacted: redactPhoneNumber(toNumber),
|
|
103
|
-
toNumberHash
|
|
104
|
-
toNumberOptOutHash
|
|
114
|
+
toNumberHash,
|
|
115
|
+
toNumberOptOutHash,
|
|
116
|
+
approvalIdentity,
|
|
105
117
|
workflowId,
|
|
106
118
|
purpose,
|
|
107
119
|
mode,
|
|
108
|
-
|
|
120
|
+
// Dograh outbound calls always require a consumable Command Post approval.
|
|
121
|
+
// The config flag is retained for backward-compatible config parsing but
|
|
122
|
+
// must not create an unapproved dialing path.
|
|
123
|
+
requiresApproval: true,
|
|
109
124
|
telephonyConfigurationId: optionalPositiveInt(input.telephonyConfigurationId),
|
|
110
125
|
fromPhoneNumberId: optionalPositiveInt(input.fromPhoneNumberId)
|
|
111
126
|
};
|
|
112
127
|
}
|
|
113
128
|
export {
|
|
129
|
+
buildTelephonyApprovalIdentity,
|
|
114
130
|
hashPhoneForApproval,
|
|
115
131
|
hashPhoneForOptOut,
|
|
116
132
|
normalizePhoneNumber,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/telephony/phonePolicy.ts"],"sourcesContent":["import { createHmac } from 'crypto';\nimport type { TitanConfig } from '../config/schema.js';\n\nexport type OutboundCallMode = 'test' | 'callback' | 'campaign';\n\nexport interface OutboundCallPolicyInput {\n toNumber?: unknown;\n workflowId?: unknown;\n purpose?: unknown;\n mode?: unknown;\n telephonyConfigurationId?: unknown;\n fromPhoneNumberId?: unknown;\n}\n\nexport interface OutboundCallPolicyResult {\n ok: boolean;\n reason?: string;\n toNumber?: string;\n toNumberRedacted?: string;\n toNumberHash?: string;\n toNumberOptOutHash?: string;\n workflowId?: string;\n purpose?: string;\n mode?: OutboundCallMode;\n requiresApproval?: boolean;\n telephonyConfigurationId?: number;\n fromPhoneNumberId?: number;\n}\n\ntype TelephonyConfig = TitanConfig['telephony'];\n\nconst E164_RE = /^\\+[1-9]\\d{7,14}$/;\nconst SAFE_PURPOSE_MAX = 160;\n\nexport function normalizePhoneNumber(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (trimmed.startsWith('+')) {\n const normalized = `+${trimmed.slice(1).replace(/\\D/g, '')}`;\n return E164_RE.test(normalized) ? normalized : null;\n }\n return null;\n}\n\nexport function redactPhoneNumber(value: string | undefined | null): string {\n const normalized = normalizePhoneNumber(value) ?? '';\n if (!normalized) return '[invalid-phone]';\n if (normalized.length <= 8) return `${normalized.slice(0, 2)}••${normalized.slice(-2)}`;\n return `${normalized.slice(0, 5)}••••${normalized.slice(-2)}`;\n}\n\nexport function hashPhoneForApproval(config: TelephonyConfig, normalizedPhoneNumber: string): string {\n const key = config.dograh.apiKey || 'titan-telephony-approval';\n return createHmac('sha256', key).update(normalizedPhoneNumber).digest('hex');\n}\n\nexport function hashPhoneForOptOut(normalizedPhoneNumber: string): string {\n return createHmac('sha256', 'titan-telephony-opt-out-v1').update(normalizedPhoneNumber).digest('hex');\n}\n\nfunction normalizeMode(value: unknown): OutboundCallMode {\n return value === 'campaign' || value === 'callback' || value === 'test' ? value : 'callback';\n}\n\nfunction safePurpose(value: unknown): string {\n if (typeof value !== 'string') return '';\n return value.replace(/[\\r\\n\\t]+/g, ' ').replace(/\\+\\d[\\d\\s().-]{6,}\\d/g, '[phone]').slice(0, SAFE_PURPOSE_MAX).trim();\n}\n\nfunction optionalPositiveInt(value: unknown): number | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isInteger(n) && n > 0 ? n : undefined;\n}\n\nexport function validateOutboundCallRequest(\n config: TelephonyConfig,\n input: OutboundCallPolicyInput,\n): OutboundCallPolicyResult {\n if (!config.enabled) {\n return { ok: false, reason: 'Telephony is disabled.' };\n }\n if (!config.dograh.apiKey) {\n return { ok: false, reason: 'Dograh API key is not configured.' };\n }\n\n const toNumber = normalizePhoneNumber(input.toNumber);\n if (!toNumber) {\n return { ok: false, reason: 'Outbound call requires a valid E.164 destination number.' };\n }\n\n const mode = normalizeMode(input.mode);\n if (mode === 'campaign' && !config.allowCampaigns) {\n return {\n ok: false,\n reason: 'Campaign outbound calls are disabled. Enable telephony.allowCampaigns and compliance controls first.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n toNumberHash: hashPhoneForApproval(config, toNumber),\n toNumberOptOutHash: hashPhoneForOptOut(toNumber),\n mode,\n };\n }\n\n if (mode === 'campaign') {\n if (!config.recordingDisclosure.trim()) {\n return {\n ok: false,\n reason: 'Campaign outbound calls require a recording disclosure before dialing.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n mode,\n };\n }\n if (!config.optOutKeywords.some((keyword) => keyword.trim())) {\n return {\n ok: false,\n reason: 'Campaign outbound calls require at least one opt-out keyword before dialing.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n mode,\n };\n }\n if (!Number.isInteger(config.maxCallsPerHour) || config.maxCallsPerHour < 1) {\n return {\n ok: false,\n reason: 'Campaign outbound calls require telephony.maxCallsPerHour to be at least 1.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n mode,\n };\n }\n }\n\n const workflowId = typeof input.workflowId === 'string' && input.workflowId.trim()\n ? input.workflowId.trim()\n : config.dograh.defaultOutboundWorkflowId;\n if (!workflowId) {\n return {\n ok: false,\n reason: 'Dograh outbound workflow ID is not configured.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n toNumberHash: hashPhoneForApproval(config, toNumber),\n toNumberOptOutHash: hashPhoneForOptOut(toNumber),\n mode,\n };\n }\n\n const purpose = safePurpose(input.purpose) || 'Outbound Dograh call';\n return {\n ok: true,\n toNumber,\n toNumberRedacted: redactPhoneNumber(toNumber),\n toNumberHash
|
|
1
|
+
{"version":3,"sources":["../../src/telephony/phonePolicy.ts"],"sourcesContent":["import { createHmac } from 'crypto';\nimport type { TitanConfig } from '../config/schema.js';\n\nexport type OutboundCallMode = 'test' | 'callback' | 'campaign';\n\nexport interface OutboundCallPolicyInput {\n toNumber?: unknown;\n workflowId?: unknown;\n purpose?: unknown;\n mode?: unknown;\n telephonyConfigurationId?: unknown;\n fromPhoneNumberId?: unknown;\n}\n\nexport interface OutboundCallPolicyResult {\n ok: boolean;\n reason?: string;\n toNumber?: string;\n toNumberRedacted?: string;\n toNumberHash?: string;\n toNumberOptOutHash?: string;\n approvalIdentity?: string;\n workflowId?: string;\n purpose?: string;\n mode?: OutboundCallMode;\n requiresApproval?: boolean;\n telephonyConfigurationId?: number;\n fromPhoneNumberId?: number;\n}\n\ntype TelephonyConfig = TitanConfig['telephony'];\n\nconst E164_RE = /^\\+[1-9]\\d{7,14}$/;\nconst SAFE_PURPOSE_MAX = 160;\n\nexport function normalizePhoneNumber(value: unknown): string | null {\n if (typeof value !== 'string') return null;\n const trimmed = value.trim();\n if (trimmed.startsWith('+')) {\n const normalized = `+${trimmed.slice(1).replace(/\\D/g, '')}`;\n return E164_RE.test(normalized) ? normalized : null;\n }\n return null;\n}\n\nexport function redactPhoneNumber(value: string | undefined | null): string {\n const normalized = normalizePhoneNumber(value) ?? '';\n if (!normalized) return '[invalid-phone]';\n if (normalized.length <= 8) return `${normalized.slice(0, 2)}••${normalized.slice(-2)}`;\n return `${normalized.slice(0, 5)}••••${normalized.slice(-2)}`;\n}\n\nexport function hashPhoneForApproval(config: TelephonyConfig, normalizedPhoneNumber: string): string {\n const key = config.dograh.apiKey || 'titan-telephony-approval';\n return createHmac('sha256', key).update(normalizedPhoneNumber).digest('hex');\n}\n\nexport function hashPhoneForOptOut(normalizedPhoneNumber: string): string {\n return createHmac('sha256', 'titan-telephony-opt-out-v1').update(normalizedPhoneNumber).digest('hex');\n}\n\nexport function buildTelephonyApprovalIdentity(fields: {\n mode: OutboundCallMode;\n workflowId: string;\n purpose: string;\n toNumberHash: string;\n}): string {\n return createHmac('sha256', 'titan-telephony-approval-identity-v1')\n .update(JSON.stringify({\n mode: fields.mode,\n workflowId: fields.workflowId,\n purpose: fields.purpose,\n toNumberHash: fields.toNumberHash,\n }))\n .digest('hex');\n}\n\nfunction normalizeMode(value: unknown): OutboundCallMode {\n return value === 'campaign' || value === 'callback' || value === 'test' ? value : 'callback';\n}\n\nfunction safePurpose(value: unknown): string {\n if (typeof value !== 'string') return '';\n return value.replace(/[\\r\\n\\t]+/g, ' ').replace(/\\+\\d[\\d\\s().-]{6,}\\d/g, '[phone]').slice(0, SAFE_PURPOSE_MAX).trim();\n}\n\nfunction optionalPositiveInt(value: unknown): number | undefined {\n if (value === undefined || value === null || value === '') return undefined;\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isInteger(n) && n > 0 ? n : undefined;\n}\n\nexport function validateOutboundCallRequest(\n config: TelephonyConfig,\n input: OutboundCallPolicyInput,\n): OutboundCallPolicyResult {\n if (!config.enabled) {\n return { ok: false, reason: 'Telephony is disabled.' };\n }\n if (!config.dograh.apiKey) {\n return { ok: false, reason: 'Dograh API key is not configured.' };\n }\n\n const toNumber = normalizePhoneNumber(input.toNumber);\n if (!toNumber) {\n return { ok: false, reason: 'Outbound call requires a valid E.164 destination number.' };\n }\n\n const mode = normalizeMode(input.mode);\n if (mode === 'campaign' && !config.allowCampaigns) {\n return {\n ok: false,\n reason: 'Campaign outbound calls are disabled. Enable telephony.allowCampaigns and compliance controls first.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n toNumberHash: hashPhoneForApproval(config, toNumber),\n toNumberOptOutHash: hashPhoneForOptOut(toNumber),\n mode,\n };\n }\n\n if (mode === 'campaign') {\n if (!config.recordingDisclosure.trim()) {\n return {\n ok: false,\n reason: 'Campaign outbound calls require a recording disclosure before dialing.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n mode,\n };\n }\n if (!config.optOutKeywords.some((keyword) => keyword.trim())) {\n return {\n ok: false,\n reason: 'Campaign outbound calls require at least one opt-out keyword before dialing.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n mode,\n };\n }\n if (!Number.isInteger(config.maxCallsPerHour) || config.maxCallsPerHour < 1) {\n return {\n ok: false,\n reason: 'Campaign outbound calls require telephony.maxCallsPerHour to be at least 1.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n mode,\n };\n }\n }\n\n const workflowId = typeof input.workflowId === 'string' && input.workflowId.trim()\n ? input.workflowId.trim()\n : config.dograh.defaultOutboundWorkflowId;\n if (!workflowId) {\n return {\n ok: false,\n reason: 'Dograh outbound workflow ID is not configured.',\n toNumberRedacted: redactPhoneNumber(toNumber),\n toNumberHash: hashPhoneForApproval(config, toNumber),\n toNumberOptOutHash: hashPhoneForOptOut(toNumber),\n mode,\n };\n }\n\n const purpose = safePurpose(input.purpose) || 'Outbound Dograh call';\n const toNumberHash = hashPhoneForApproval(config, toNumber);\n const toNumberOptOutHash = hashPhoneForOptOut(toNumber);\n const approvalIdentity = buildTelephonyApprovalIdentity({ mode, workflowId, purpose, toNumberHash });\n return {\n ok: true,\n toNumber,\n toNumberRedacted: redactPhoneNumber(toNumber),\n toNumberHash,\n toNumberOptOutHash,\n approvalIdentity,\n workflowId,\n purpose,\n mode,\n // Dograh outbound calls always require a consumable Command Post approval.\n // The config flag is retained for backward-compatible config parsing but\n // must not create an unapproved dialing path.\n requiresApproval: true,\n telephonyConfigurationId: optionalPositiveInt(input.telephonyConfigurationId),\n fromPhoneNumberId: optionalPositiveInt(input.fromPhoneNumberId),\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/utils/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
const TITAN_VERSION = "6.2.
|
|
4
|
+
const TITAN_VERSION = "6.2.1";
|
|
5
5
|
const TITAN_CODENAME = "Living Canvas";
|
|
6
6
|
const TITAN_NAME = "TITAN";
|
|
7
7
|
const TITAN_FULL_NAME = "The Intelligent Task Automation Network";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '6.2.
|
|
1
|
+
{"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '6.2.1';\nexport const TITAN_CODENAME = 'Living Canvas';\nexport const TITAN_NAME = 'TITAN';\nexport const TITAN_FULL_NAME = 'The Intelligent Task Automation Network';\nexport const TITAN_ASCII_LOGO = `\n╔══════════════════════════════════════════════════════╗\n║ ║\n║ ████████╗██╗████████╗ █████╗ ███╗ ██╗ ║\n║ ██║ ██║ ██║ ██╔══██╗████╗ ██║ ║\n║ ██║ ██║ ██║ ███████║██╔██╗ ██║ ║\n║ ██║ ██║ ██║ ██╔══██║██║╚██╗██║ ║\n║ ██║ ██║ ██║ ██║ ██║██║ ╚████║ ║\n║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ║\n║ ║\n║ The Intelligent Task Automation Network ║\n║ v${TITAN_VERSION} • by Tony Elliott ║\n╚══════════════════════════════════════════════════════╝`;\n\n// Paths\n// Hunt Finding #03 (2026-04-14): honor TITAN_HOME env var if set.\n// Previously this was hardcoded to `~/.titan`, which meant:\n// - Docker containers couldn't override the config path\n// - Shared machines couldn't isolate per-user state\n// - Test fixtures couldn't run against an isolated home\n// - The systemd unit's `Environment=TITAN_HOME=...` was silently ignored\n// The env var is read once at module load (constants are resolved at import time).\n// If TITAN_HOME starts with `~/`, expand it to the user's home dir.\nfunction resolveTitanHome(): string {\n const envHome = process.env.TITAN_HOME;\n if (envHome && envHome.trim().length > 0) {\n const trimmed = envHome.trim();\n if (trimmed.startsWith('~/')) {\n return join(homedir(), trimmed.slice(2));\n }\n if (trimmed === '~') {\n return homedir();\n }\n return trimmed;\n }\n return join(homedir(), '.titan');\n}\nexport const TITAN_HOME = resolveTitanHome();\nexport const TITAN_CONFIG_PATH = join(TITAN_HOME, 'titan.json');\nexport const TITAN_DB_PATH = join(TITAN_HOME, 'titan.db');\nexport const TITAN_WORKSPACE = join(TITAN_HOME, 'workspace');\nexport const TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, 'skills');\nexport const TITAN_LOGS_DIR = join(TITAN_HOME, 'logs');\nexport const TITAN_MEMORY_DIR = join(TITAN_HOME, 'memory');\n\n// Workspace prompt files (injected into agent context)\nexport const AGENTS_MD = join(TITAN_WORKSPACE, 'AGENTS.md');\nexport const SOUL_MD = join(TITAN_WORKSPACE, 'SOUL.md');\nexport const TOOLS_MD = join(TITAN_WORKSPACE, 'TOOLS.md');\nexport const TITAN_MD_FILENAME = 'TITAN.md';\nexport const AUTOPILOT_MD = join(TITAN_HOME, 'AUTOPILOT.md');\nexport const AUTOPILOT_RUNS_PATH = join(TITAN_HOME, 'autopilot-runs.jsonl');\nexport const TITAN_CREDENTIALS_DIR = join(TITAN_HOME, 'credentials');\n\n// Income & lead tracking\nexport const INCOME_LEDGER_PATH = join(TITAN_HOME, 'income-ledger.jsonl');\nexport const FREELANCE_LEADS_PATH = join(TITAN_HOME, 'freelance-leads.jsonl');\nexport const FREELANCE_PROFILE_PATH = join(TITAN_HOME, 'freelance-profile.json');\nexport const LEADS_PATH = join(TITAN_HOME, 'leads.jsonl');\nexport const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, 'telemetry-events.jsonl');\nexport const SOMADRIVE_STATE_PATH = join(TITAN_HOME, 'soma-drive-state.json');\nexport const ACTIVITY_LOG_PATH = join(TITAN_HOME, 'activity-log.jsonl');\n\n// v6.1.0 — Mission Chat\nexport const MISSIONS_DIR = join(TITAN_HOME, 'missions');\nexport const PLAYS_DIR = join(TITAN_HOME, 'plays');\n\n// Gateway defaults\nexport const DEFAULT_GATEWAY_HOST = '0.0.0.0';\nexport const DEFAULT_GATEWAY_PORT = 48420;\nexport const DEFAULT_WEB_PORT = 48421;\n\n// Agent defaults\n// v6.0.1 — Hardcoded floor only. The actual default at runtime is picked\n// by `getDefaultModelId()` in `src/providers/defaultModel.ts`, which\n// detects the user's available API keys (Anthropic / OpenAI / Google /\n// OpenRouter) and selects accordingly. Falls back to local Ollama when\n// no cloud keys are set. This constant is the last-resort fallback for\n// callsites that don't go through the schema thunk.\nexport const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';\n/** v5.4.1: User-preference ceiling. Providers clamp per-model via\n * clampMaxTokens() so this can be high without causing 400s on\n * capped endpoints (e.g. Claude Sonnet 4 8K, Cohere 4K). */\nexport const DEFAULT_MAX_TOKENS = 200000;\nexport const DEFAULT_TEMPERATURE = 0.7;\nexport const MAX_CONTEXT_MESSAGES = 50;\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\n\n// Security\nexport const DEFAULT_SANDBOX_MODE = 'host';\n/** Default allowed tools. Empty = allow ALL registered tools.\n * Use security.deniedTools to block specific tools instead. */\nexport const ALLOWED_TOOLS_DEFAULT: string[] = [];\nexport const DENIED_TOOLS_DEFAULT: string[] = [];\n"],"mappings":";AAGA,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAW1B,aAAa;AAAA;AAYnB,SAAS,mBAA2B;AAChC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACtC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC1B,aAAO,KAAK,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3C;AACA,QAAI,YAAY,KAAK;AACjB,aAAO,QAAQ;AAAA,IACnB;AACA,WAAO;AAAA,EACX;AACA,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACO,MAAM,aAAa,iBAAiB;AACpC,MAAM,oBAAoB,KAAK,YAAY,YAAY;AACvD,MAAM,gBAAgB,KAAK,YAAY,UAAU;AACjD,MAAM,kBAAkB,KAAK,YAAY,WAAW;AACpD,MAAM,mBAAmB,KAAK,iBAAiB,QAAQ;AACvD,MAAM,iBAAiB,KAAK,YAAY,MAAM;AAC9C,MAAM,mBAAmB,KAAK,YAAY,QAAQ;AAGlD,MAAM,YAAY,KAAK,iBAAiB,WAAW;AACnD,MAAM,UAAU,KAAK,iBAAiB,SAAS;AAC/C,MAAM,WAAW,KAAK,iBAAiB,UAAU;AACjD,MAAM,oBAAoB;AAC1B,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,MAAM,sBAAsB,KAAK,YAAY,sBAAsB;AACnE,MAAM,wBAAwB,KAAK,YAAY,aAAa;AAG5D,MAAM,qBAAqB,KAAK,YAAY,qBAAqB;AACjE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,yBAAyB,KAAK,YAAY,wBAAwB;AACxE,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,MAAM,wBAAwB,KAAK,YAAY,wBAAwB;AACvE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,oBAAoB,KAAK,YAAY,oBAAoB;AAG/D,MAAM,eAAe,KAAK,YAAY,UAAU;AAChD,MAAM,YAAY,KAAK,YAAY,OAAO;AAG1C,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AASzB,MAAM,gBAAgB;AAItB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,KAAK;AAGrC,MAAM,uBAAuB;AAG7B,MAAM,wBAAkC,CAAC;AACzC,MAAM,uBAAiC,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "titan-agent",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.1",
|
|
4
4
|
"description": "TITAN — TypeScript AI agent framework with multi-agent orchestration, provider routing, approval gates, receipts, evals, gateway APIs, and a React Mission Control dashboard. Open-source, MIT licensed.",
|
|
5
5
|
"author": "Tony Elliott (https://github.com/Djtony707)",
|
|
6
6
|
"repository": {
|
package/ui/dist/sw.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* but a default falls back to the source-controlled value here.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
const CACHE_NAME = 'titan-' + ('
|
|
23
|
+
const CACHE_NAME = 'titan-' + ('1779149157901');
|
|
24
24
|
const ASSETS_PREFIX = '/assets/';
|
|
25
25
|
|
|
26
26
|
self.addEventListener('install', (event) => {
|