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.
@@ -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 { hashPhoneForApproval, normalizePhoneNumber, redactPhoneNumber, validateOutboundCallRequest } from "../../telephony/phonePolicy.js";
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 = readReceipts({ kind: "approval_decision", limit: 1e3 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === "ok");
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: ${receipt.action_id}`,
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,UACrC;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
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: hashPhoneForApproval(config, toNumber),
104
- toNumberOptOutHash: hashPhoneForOptOut(toNumber),
114
+ toNumberHash,
115
+ toNumberOptOutHash,
116
+ approvalIdentity,
105
117
  workflowId,
106
118
  purpose,
107
119
  mode,
108
- requiresApproval: config.requireApprovalForOutbound || mode === "campaign",
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: hashPhoneForApproval(config, toNumber),\n toNumberOptOutHash: hashPhoneForOptOut(toNumber),\n workflowId,\n purpose,\n mode,\n requiresApproval: config.requireApprovalForOutbound || mode === 'campaign',\n telephonyConfigurationId: optionalPositiveInt(input.telephonyConfigurationId),\n fromPhoneNumberId: optionalPositiveInt(input.fromPhoneNumberId),\n };\n}\n"],"mappings":"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","names":[]}
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":[]}
@@ -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.0";
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.0';\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":[]}
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.0",
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-' + ('1779138804143');
23
+ const CACHE_NAME = 'titan-' + ('1779149157901');
24
24
  const ASSETS_PREFIX = '/assets/';
25
25
 
26
26
  self.addEventListener('install', (event) => {