titan-agent 6.1.0-beta.2 → 6.1.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -29
- package/README.md +1 -1
- package/dist/agent/agentLessons.js +2 -2
- package/dist/agent/agentLessons.js.map +1 -1
- package/dist/agent/autoModeClassifier.js +95 -0
- package/dist/agent/autoModeClassifier.js.map +1 -0
- package/dist/agent/durableJournal.js +2 -2
- package/dist/agent/durableJournal.js.map +1 -1
- package/dist/agent/goalDriver.js +67 -0
- package/dist/agent/goalDriver.js.map +1 -1
- package/dist/agent/goalPlanFile.js +3 -3
- package/dist/agent/goalPlanFile.js.map +1 -1
- package/dist/agent/missionLifecycle.js +2 -2
- package/dist/agent/missionLifecycle.js.map +1 -1
- package/dist/agent/subAgent.js +41 -0
- package/dist/agent/subAgent.js.map +1 -1
- package/dist/agent/toolContract.js +49 -0
- package/dist/agent/toolContract.js.map +1 -0
- package/dist/agent/toolRunner.js +72 -11
- package/dist/agent/toolRunner.js.map +1 -1
- package/dist/agent/trajectoryRecorder.js +185 -0
- package/dist/agent/trajectoryRecorder.js.map +1 -0
- package/dist/agent/verification.js +164 -0
- package/dist/agent/verification.js.map +1 -0
- package/dist/agent/worktreeExecutor.js +207 -0
- package/dist/agent/worktreeExecutor.js.map +1 -0
- package/dist/config/schema.js +28 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway/server.js +54 -0
- package/dist/gateway/server.js.map +1 -1
- package/dist/memory/facade.js +137 -0
- package/dist/memory/facade.js.map +1 -0
- package/dist/migrations/003-soma-profile-default.js +2 -2
- package/dist/migrations/003-soma-profile-default.js.map +1 -1
- package/dist/migrations/005-route-redirects.js +3 -1
- package/dist/migrations/005-route-redirects.js.map +1 -1
- package/dist/migrations/runner.js +2 -2
- package/dist/migrations/runner.js.map +1 -1
- package/dist/providers/defaultModel.js +14 -0
- package/dist/providers/defaultModel.js.map +1 -1
- package/dist/providers/router.js +4 -0
- package/dist/providers/router.js.map +1 -1
- package/dist/providers/stub.js +185 -0
- package/dist/providers/stub.js.map +1 -0
- package/dist/skills/builtin/backup.js +1 -2
- package/dist/skills/builtin/backup.js.map +1 -1
- package/dist/skills/builtin/contracts.js +254 -0
- package/dist/skills/builtin/contracts.js.map +1 -0
- package/dist/skills/builtin/download_image.js +9 -0
- package/dist/skills/builtin/download_image.js.map +1 -1
- package/dist/skills/builtin/filesystem.js +70 -20
- package/dist/skills/builtin/filesystem.js.map +1 -1
- package/dist/skills/registry.js +7 -0
- package/dist/skills/registry.js.map +1 -1
- package/dist/storage/patterns.js +2 -2
- package/dist/storage/patterns.js.map +1 -1
- package/dist/storage/personalGallery.js +2 -2
- package/dist/storage/personalGallery.js.map +1 -1
- package/dist/storage/somaProfile.js +2 -2
- package/dist/storage/somaProfile.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/package.json +2 -2
- package/ui/dist/assets/{AuditPanel-DrDvXZ9u.js → AuditPanel-RNrvKmfX.js} +1 -1
- package/ui/dist/assets/{AutonomyPanel-BfhW-PRX.js → AutonomyPanel-D0nmt79R.js} +2 -2
- package/ui/dist/assets/{AutopilotPanel-C3yQBxAp.js → AutopilotPanel-DlcdfRRy.js} +1 -1
- package/ui/dist/assets/{AutoresearchPanel-BgpR3EcK.js → AutoresearchPanel-CXeO6kGK.js} +2 -2
- package/ui/dist/assets/BackupPanel-CKb-cDsk.js +1 -0
- package/ui/dist/assets/{BrowserPanel-BNaKZOnn.js → BrowserPanel-CDaoF6n5.js} +1 -1
- package/ui/dist/assets/CPActivity-BZbeztg1.js +1 -0
- package/ui/dist/assets/CPAgentDetail-C9fvLxKi.js +1 -0
- package/ui/dist/assets/CPAgents-BsBA9kb2.js +1 -0
- package/ui/dist/assets/CPApprovals-Di5itVIK.js +1 -0
- package/ui/dist/assets/CPCosts-DmgckHli.js +1 -0
- package/ui/dist/assets/CPDashboard-CmSb7woK.js +1 -0
- package/ui/dist/assets/{CPFiles-CUpP5I82.js → CPFiles-Dd0NGcpX.js} +1 -1
- package/ui/dist/assets/CPGoals-DeILFq0A.js +1 -0
- package/ui/dist/assets/{CPInbox-DUW_hntB.js → CPInbox-DAtidZez.js} +2 -2
- package/ui/dist/assets/CPIssueDetail-BvwoGI57.js +1 -0
- package/ui/dist/assets/CPIssues-CZ1hIKDP.js +1 -0
- package/ui/dist/assets/{CPLayout-J0fa-it4.js → CPLayout-luPwhNpA.js} +3 -3
- package/ui/dist/assets/CPOrg-BlYp_KP6.js +8 -0
- package/ui/dist/assets/CPRuns-D0HMCjIo.js +1 -0
- package/ui/dist/assets/{CPSocial-By_FStZ4.js → CPSocial-CZaGi14s.js} +2 -2
- package/ui/dist/assets/ChannelsPanel-khiBHwx3.js +1 -0
- package/ui/dist/assets/{CheckpointsPanel-DvKUd2VF.js → CheckpointsPanel-n2y3jzzT.js} +1 -1
- package/ui/dist/assets/{CommandPostHub-CQqRYBjf.js → CommandPostHub-iWf-zt5p.js} +4 -4
- package/ui/dist/assets/{CronPanel-Brmnnu8_.js → CronPanel-BkRJl8u6.js} +1 -1
- package/ui/dist/assets/{DataTable-seaktPzB.js → DataTable-DZ2cG3Cv.js} +1 -1
- package/ui/dist/assets/{DreamPanel-CO7iSdjn.js → DreamPanel-DF6l0WB-.js} +1 -1
- package/ui/dist/assets/{EmptyState-CmK9fywh.js → EmptyState-D7xwm8hA.js} +1 -1
- package/ui/dist/assets/{EvalHarnessPanel-2RVBKbym.js → EvalHarnessPanel-BHabu49z.js} +2 -2
- package/ui/dist/assets/{EvalPanel-CqAwhFQ5.js → EvalPanel-yAesMLSP.js} +1 -1
- package/ui/dist/assets/{FileViewer-D98m-vF5.js → FileViewer-DOWUQccc.js} +52 -14
- package/ui/dist/assets/{FilesPanel-dRQM0apb.js → FilesPanel-BjuDhqaX.js} +2 -2
- package/ui/dist/assets/FleetPanel-B0ZhS2XX.js +1 -0
- package/ui/dist/assets/{HomelabPanel-DjMUrO2B.js → HomelabPanel-D373ddvj.js} +1 -1
- package/ui/dist/assets/InfraView-CI99NZbI.js +2 -0
- package/ui/dist/assets/InlineEditableField-fDk3eGVo.js +1 -0
- package/ui/dist/assets/Input-j10hONMY.js +1 -0
- package/ui/dist/assets/IntegrationsPanel-kPa5Qq-f.js +1 -0
- package/ui/dist/assets/IntelligenceView-DeKBaAlY.js +2 -0
- package/ui/dist/assets/LearningPanel-B8tqHeEZ.js +1 -0
- package/ui/dist/assets/{LogsPanel-Bs9IsTCH.js → LogsPanel-sXc__trb.js} +1 -1
- package/ui/dist/assets/McpPanel-DEr8pZwB.js +1 -0
- package/ui/dist/assets/{MemoryGraphPanel-F1P9COyY.js → MemoryGraphPanel-DZl7USJ6.js} +1 -1
- package/ui/dist/assets/MemoryWikiPanel-DTNVWvVT.js +11 -0
- package/ui/dist/assets/{MeshPanel-CgIAh5ZX.js → MeshPanel-b77zyQx4.js} +1 -1
- package/ui/dist/assets/MissionCanvas-D_quSl5F.js +69 -0
- package/ui/dist/assets/MissionChat-rQ5rUPAt.js +7 -0
- package/ui/dist/assets/MissionLibrary-Cxw7sLxv.js +5 -0
- package/ui/dist/assets/MissionStart-ChMWO1rr.js +1 -0
- package/ui/dist/assets/Modal-Dl5b6Yaj.js +1 -0
- package/ui/dist/assets/NvidiaPanel-D7qFbbeJ.js +1 -0
- package/ui/dist/assets/{OrganismPanel-CNZM3eof.js → OrganismPanel-EiQ9Gfqx.js} +1 -1
- package/ui/dist/assets/OverviewPanel-Bectj1Z5.js +1 -0
- package/ui/dist/assets/{PageHeader-llOXO43Z.js → PageHeader-CiuoyvDR.js} +1 -1
- package/ui/dist/assets/{PersonaProfilesPanel-CfsQTMra.js → PersonaProfilesPanel-BO87DSVG.js} +1 -1
- package/ui/dist/assets/{PersonasPanel-DIac_kZ3.js → PersonasPanel-BNogLwM0.js} +1 -1
- package/ui/dist/assets/RecipesPanel-DyqfWlaY.js +1 -0
- package/ui/dist/assets/{SecurityPanel-_gqMk8hJ.js → SecurityPanel-OMC-Tvbz.js} +1 -1
- package/ui/dist/assets/SelfImprovePanel-DXIfG_df.js +1 -0
- package/ui/dist/assets/{SelfProposalsPanel-DuA0zsGH.js → SelfProposalsPanel-CmgEz1TQ.js} +2 -2
- package/ui/dist/assets/SessionsPanel-gJXosEGf.js +1 -0
- package/ui/dist/assets/{SessionsTab-bFYaFvTZ.js → SessionsTab-BOVzzjTO.js} +1 -1
- package/ui/dist/assets/SettingsPanel-DifFGEvi.js +1 -0
- package/ui/dist/assets/SettingsView-BNpj0iym.js +2 -0
- package/ui/dist/assets/SkeletonLoader-CNSsfH3f.js +1 -0
- package/ui/dist/assets/{SkillsPanel-D08WcRGp.js → SkillsPanel-6fbRov-m.js} +1 -1
- package/ui/dist/assets/{SomaView-CYukE7Fp.js → SomaView-D3boPl5D.js} +1 -1
- package/ui/dist/assets/{StatCard-DT4Bx1mY.js → StatCard-2BmOlamJ.js} +1 -1
- package/ui/dist/assets/StatusBadge-CdVHF1nG.js +1 -0
- package/ui/dist/assets/Tabs-DW9ytf9H.js +1 -0
- package/ui/dist/assets/{TeamsPanel-DOBe72TK.js → TeamsPanel-CNaQ8Dbw.js} +1 -1
- package/ui/dist/assets/TelemetryPanel-DL81QlAW.js +1 -0
- package/ui/dist/assets/TitanCanvas-DLIN_wV3.js +1152 -0
- package/ui/dist/assets/ToolsView-BlChEE4n.js +2 -0
- package/ui/dist/assets/Tooltip-oR5JnwoN.js +6 -0
- package/ui/dist/assets/{TraceViewer-0bCz0x_a.js → TraceViewer-DtJXjCXV.js} +1 -1
- package/ui/dist/assets/TrainingPanel-4XMGaRN_.js +1 -0
- package/ui/dist/assets/{VoiceOverlay-B08Sw918.js → VoiceOverlay-B3fXJ-EC.js} +1 -1
- package/ui/dist/assets/{VramPanel-CJiic8y4.js → VramPanel-HisAYiqI.js} +1 -1
- package/ui/dist/assets/{WatchView-CaQMDEAy.js → WatchView-C92y0hU7.js} +1 -1
- package/ui/dist/assets/{WorkTab-D5bi2WRi.js → WorkTab-BYAWcvA1.js} +1 -1
- package/ui/dist/assets/{WorkflowsPanel-DdY6E93C.js → WorkflowsPanel-D2DiOBLA.js} +1 -1
- package/ui/dist/assets/{arrow-left-CHJyrO8S.js → arrow-left-thXQMJQ6.js} +1 -1
- package/ui/dist/assets/{briefcase-ROIEmiah.js → briefcase-ByMhDqlL.js} +1 -1
- package/ui/dist/assets/{chart-column-wFkjV3Ni.js → chart-column-CYHJVuxN.js} +1 -1
- package/ui/dist/assets/{check-BcOm5jPO.js → check-ChWLFWCx.js} +1 -1
- package/ui/dist/assets/{chevron-down-CM11GPpR.js → chevron-down-CU_O_5h3.js} +1 -1
- package/ui/dist/assets/{chevron-right-aFDDgFsL.js → chevron-right-DAiXsK24.js} +1 -1
- package/ui/dist/assets/{chevron-up-D6g7FB6i.js → chevron-up-BudRYwvy.js} +1 -1
- package/ui/dist/assets/{circle-check-big-DPz64kZW.js → circle-check-big-gkA-V2JW.js} +1 -1
- package/ui/dist/assets/{clock-BbCCaukN.js → clock-qO5kIED9.js} +1 -1
- package/ui/dist/assets/clsx-B-dksMZM.js +1 -0
- package/ui/dist/assets/{dollar-sign-Dy5-qkg3.js → dollar-sign-1gQNJK3V.js} +1 -1
- package/ui/dist/assets/{download-DjheLEns.js → download-DMD1m87c.js} +1 -1
- package/ui/dist/assets/{external-link-BgxGF7ZQ.js → external-link-ByafQkZ7.js} +1 -1
- package/ui/dist/assets/{folder-DY4eeMdo.js → folder-DeG4RuOD.js} +1 -1
- package/ui/dist/assets/{funnel-LWmcwhj-.js → funnel-wE8i5V9Y.js} +1 -1
- package/ui/dist/assets/{git-branch-CPZciK10.js → git-branch-BWSuK1LU.js} +1 -1
- package/ui/dist/assets/index-B8YEFBRH.js +277 -0
- package/ui/dist/assets/index-Cb5bSxRB.css +1 -0
- package/ui/dist/assets/{layers-Bn7bgu2Z.js → layers-tN-wIpE0.js} +1 -1
- package/ui/dist/assets/{legacy-BVPeht6t.js → legacy-BSZBdyop.js} +2 -2
- package/ui/dist/assets/{lightbulb-tzuCyEQk.js → lightbulb-TUHKCda3.js} +1 -1
- package/ui/dist/assets/{list-todo-say4NbSw.js → list-todo-DtxcUGz2.js} +1 -1
- package/ui/dist/assets/{missions-DbC8UmMc.js → missions-C3EC4uKT.js} +1 -1
- package/ui/dist/assets/{network-v9O7MmZT.js → network-ephW7nah.js} +1 -1
- package/ui/dist/assets/{pause-CZo33QlA.js → pause-DeggUVZc.js} +1 -1
- package/ui/dist/assets/{play-DbCJhc5H.js → play-Dfc0ia6v.js} +1 -1
- package/ui/dist/assets/{plug-CtpCZg2z.js → plug-Bl_6i_9u.js} +1 -1
- package/ui/dist/assets/{plus-O61rB_YF.js → plus-CQwCh7WY.js} +1 -1
- package/ui/dist/assets/{proxy-BAUTNyg6.js → proxy-BjQ4lsww.js} +1 -1
- package/ui/dist/assets/{rotate-ccw-n5WkWR51.js → rotate-ccw-CUuN2ebi.js} +1 -1
- package/ui/dist/assets/{save-D2Qh8KyN.js → save-PlTUvsN5.js} +1 -1
- package/ui/dist/assets/{search-C9SdcZJq.js → search-BHgLDT-j.js} +1 -1
- package/ui/dist/assets/{send-DZ_5M5QQ.js → send-pflQNBoT.js} +1 -1
- package/ui/dist/assets/{shield-check-DyP5PmaY.js → shield-check-CrD-s89i.js} +1 -1
- package/ui/dist/assets/{target-CWfn705s.js → target-D4oh8GME.js} +1 -1
- package/ui/dist/assets/{terminal-CdhfA0Kr.js → terminal-BvJZ1R3x.js} +1 -1
- package/ui/dist/assets/{toggle-right-CmYQLQRS.js → toggle-right-Dl7x_ed7.js} +1 -1
- package/ui/dist/assets/{trash-2-TQcEJEv4.js → trash-2-lPcFwnW5.js} +1 -1
- package/ui/dist/assets/{trending-up-Cw22bM4J.js → trending-up-DqUbZfDe.js} +1 -1
- package/ui/dist/assets/{trophy-DUF8Bcas.js → trophy-Dv6zFfKM.js} +1 -1
- package/ui/dist/assets/{users-BX_ZaByZ.js → users-NPyAWbs5.js} +1 -1
- package/ui/dist/assets/{wrench-Cfp7nYhG.js → wrench-BCKgeHj3.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +1 -1
- package/ui/dist/assets/BackupPanel-BNxMlNIP.js +0 -1
- package/ui/dist/assets/CPActivity-CjIgehOq.js +0 -1
- package/ui/dist/assets/CPAgentDetail-oaymYRNI.js +0 -1
- package/ui/dist/assets/CPAgents-CWQ6iJfO.js +0 -1
- package/ui/dist/assets/CPApprovals-DzATBhtM.js +0 -1
- package/ui/dist/assets/CPCosts-xVQR148_.js +0 -1
- package/ui/dist/assets/CPDashboard-Dt6Jr7Zw.js +0 -1
- package/ui/dist/assets/CPGoals-Bt1a3C9T.js +0 -1
- package/ui/dist/assets/CPIssueDetail-DHxqtiuh.js +0 -1
- package/ui/dist/assets/CPIssues-D0l3fgUs.js +0 -1
- package/ui/dist/assets/CPOrg-DQ3tbYld.js +0 -8
- package/ui/dist/assets/CPRuns-0vncicUW.js +0 -1
- package/ui/dist/assets/ChannelsPanel-CiJs4gyw.js +0 -1
- package/ui/dist/assets/FleetPanel-Bg-f3_ip.js +0 -1
- package/ui/dist/assets/InfraView-CHYaZqMK.js +0 -2
- package/ui/dist/assets/InlineEditableField-BOLRKQq1.js +0 -1
- package/ui/dist/assets/Input-4pNffAw-.js +0 -1
- package/ui/dist/assets/IntegrationsPanel-CFucvr-Q.js +0 -1
- package/ui/dist/assets/IntelligenceView-Dh-Wuf13.js +0 -2
- package/ui/dist/assets/LearningPanel-CtlougkF.js +0 -1
- package/ui/dist/assets/McpPanel-BobqpLsh.js +0 -1
- package/ui/dist/assets/MemoryWikiPanel-BfYksjoI.js +0 -11
- package/ui/dist/assets/MissionCanvas-0V_vtqiu.js +0 -50
- package/ui/dist/assets/MissionChat-9hGhc9Zw.js +0 -7
- package/ui/dist/assets/MissionLibrary-dFkT8pGp.js +0 -5
- package/ui/dist/assets/MissionStart-CZSEie0Q.js +0 -1
- package/ui/dist/assets/Modal-V2yNnkKk.js +0 -1
- package/ui/dist/assets/NvidiaPanel-qLJysI9M.js +0 -1
- package/ui/dist/assets/OverviewPanel-Be6LukJ_.js +0 -1
- package/ui/dist/assets/RecipesPanel-DDYOCx3w.js +0 -1
- package/ui/dist/assets/SelfImprovePanel-B8o_fO30.js +0 -1
- package/ui/dist/assets/SessionsPanel-DeCBkSgY.js +0 -1
- package/ui/dist/assets/SettingsPanel-BlsRDAt7.js +0 -1
- package/ui/dist/assets/SettingsView-Dsa7Zr4e.js +0 -2
- package/ui/dist/assets/SkeletonLoader-BkQoZllJ.js +0 -1
- package/ui/dist/assets/StatusBadge-BFEHCdgi.js +0 -1
- package/ui/dist/assets/Tabs-g2Gs9sK0.js +0 -1
- package/ui/dist/assets/TelemetryPanel-DXAGuV0R.js +0 -1
- package/ui/dist/assets/TitanCanvas-DCl3gxSl.js +0 -1072
- package/ui/dist/assets/ToolsView-CnUjq0Js.js +0 -2
- package/ui/dist/assets/Tooltip-DJ99YlFF.js +0 -6
- package/ui/dist/assets/TrainingPanel-BLf_KMaE.js +0 -1
- package/ui/dist/assets/eye-off-CYR4nH5I.js +0 -6
- package/ui/dist/assets/index-PwwKjgT3.css +0 -1
- package/ui/dist/assets/index-slWylU2o.js +0 -244
- package/ui/dist/assets/loader-circle-E8aLxmMz.js +0 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/stub.ts"],"sourcesContent":["/**\n * TITAN — CI Stub Provider (v6.1.0-beta.11)\n *\n * Deterministic pattern-matched LLM provider used by CI eval suites\n * and offline tests. Returns canned, useful responses without making\n * any network call — so the Eval Gate workflow can run on every PR\n * without burning Anthropic / OpenAI credits OR requiring secrets\n * configured on the runner.\n *\n * WHAT this is:\n * - A real LLMProvider implementation registered as `'stub'`.\n * - Model IDs `stub/echo`, `stub/json`, `stub/refuse` route here.\n * - Pattern-matches the last user message + system prompt against a\n * small table of recognized shapes (chat, structured-JSON,\n * tool-call, safety-refusal) and emits the appropriate response.\n *\n * WHY it exists:\n * - Phase B.1 truth-table audit showed Eval Gate runs were scoring\n * 0% on every suite because no provider had credentials in CI.\n * - The fix isn't \"give CI an API key\" — that bakes a real-money\n * dependency into every push. The fix is a provider that can\n * stand in for the real model deterministically.\n * - The harness-engineering catalog (Picrew/awesome-agent-harness)\n * explicitly recommends a \"stub mode\" so the harness itself is\n * testable independent of any single LLM.\n *\n * TRADE-OFFS we deliberately accept:\n * - The stub can't answer open-ended questions. It pattern-matches\n * a fixed vocabulary. Suites that probe model creativity have\n * no business running against the stub.\n * - The stub knows the exact shapes our agent code expects (widget\n * gate JSON, structuredSpawn JSON, tool-call format). When those\n * shapes change, this file changes too. That coupling is OK —\n * it's the price of CI-grade determinism.\n *\n * FOLLOW-UP:\n * - When we add a new gated agent shape, add a recognizer here.\n * - When we add a new built-in tool, add a recognizer here so\n * tool-routing evals can exercise the dispatch path.\n */\nimport { LLMProvider } from './base.js';\nimport type {\n ChatMessage,\n ChatOptions,\n ChatResponse,\n ChatStreamChunk,\n ToolCall,\n} from './base.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'provider.stub';\n\n/* ─────────────────────────── Pattern recognizers ─────────────────────────── */\n\ninterface RecognizedIntent {\n /** Stable id of the matched pattern. Used by tests + telemetry. */\n kind: string;\n /** Plain-text body of the response. */\n content: string;\n /** Optional tool calls to emit alongside the text. */\n toolCalls?: ToolCall[];\n /** finish_reason on the response. */\n finishReason: 'stop' | 'tool_calls' | 'error';\n}\n\n/**\n * Inspect the conversation and decide what kind of response to emit.\n * Order matters — safety refusals first, then structured outputs,\n * then tool-call shapes, then default chat.\n */\nfunction recognize(messages: ChatMessage[], hasTools: boolean): RecognizedIntent {\n const lastUser = [...messages].reverse().find(m => m.role === 'user')?.content ?? '';\n const system = messages.find(m => m.role === 'system')?.content ?? '';\n const combined = `${system}\\n${lastUser}`.toLowerCase();\n const userOnly = lastUser.toLowerCase();\n\n // ── Safety refusal — keep first so injection attempts in the\n // system prompt don't override it.\n if (\n /\\b(rm\\s+-rf|delete\\s+everything|wipe\\s+the\\s+disk|drop\\s+all\\s+tables|format\\s+the\\s+drive)\\b/i.test(lastUser)\n ) {\n return {\n kind: 'safety_refusal',\n content: 'Stub: I won\\'t execute destructive system commands. Ask the user to confirm and run those manually.',\n finishReason: 'stop',\n };\n }\n\n // ── Structured JSON output — when the system prompt is asking\n // for the structuredSpawn JSON envelope. The driver loop\n // relies on this shape so the \"done\" path is reachable in CI.\n // Recognizers are deliberately broad: any prompt asking for\n // JSON output, or any mention of the spawn-envelope fields\n // (\"status\", \"artifacts\", \"confidence\") together, triggers\n // the structured-output path.\n const mentionsJsonOutput = /(respond|reply|return|output)\\s+(with\\s+)?(strict\\s+)?json|json\\s+containing|json\\s+envelope|structuredSpawn|spawn\\s+envelope/i.test(combined);\n const mentionsEnvelopeFields =\n /\"status\"/i.test(combined) &&\n /\"artifacts\"/i.test(combined) &&\n /\"confidence\"/i.test(combined);\n if (mentionsJsonOutput || mentionsEnvelopeFields) {\n const json = JSON.stringify({\n status: 'done',\n artifacts: [],\n questions: [],\n confidence: 0.9,\n reasoning: `Stub: pattern-matched structured-output request on prompt \"${userOnly.slice(0, 60)}\".`,\n });\n return {\n kind: 'structured_done',\n content: '```json\\n' + json + '\\n```',\n finishReason: 'stop',\n };\n }\n\n // ── Widget gate — TITAN's chat surface listens for a\n // `_____widget` gate followed by a JSON line. The 100+ widget\n // gallery routes off this signal; eval suites assert it.\n if (/widget|panel|gauge|dashboard|stock\\s+ticker|pomodoro|calendar/i.test(userOnly)) {\n const widgetName = pickWidget(userOnly);\n const widgetJson = JSON.stringify({\n name: widgetName,\n format: 'system',\n source: 'gallery',\n w: 360,\n h: 240,\n });\n return {\n kind: 'widget_gate',\n content: `Adding the **${widgetName}** widget to your canvas.\\n\\n_____widget\\n${widgetJson}`,\n finishReason: 'stop',\n };\n }\n\n // ── Tool-call shape — when tools are offered AND the user prompt\n // clearly asks for one. Emits a tool_calls finish_reason so\n // the agent loop dispatches the tool. The agent will then\n // feed a tool_result back; that's handled below.\n if (hasTools) {\n const toolMatch = pickTool(userOnly);\n if (toolMatch) {\n return {\n kind: `tool_call:${toolMatch.name}`,\n content: '',\n toolCalls: [{\n id: `stub-tool-${Date.now()}`,\n type: 'function',\n function: {\n name: toolMatch.name,\n arguments: JSON.stringify(toolMatch.args),\n },\n }],\n finishReason: 'tool_calls',\n };\n }\n }\n\n // ── Tool-result continuation — if the LAST message is a tool\n // result, we wrap up with a brief acknowledgement so the\n // multi-round loop terminates cleanly.\n if (messages[messages.length - 1]?.role === 'tool') {\n return {\n kind: 'tool_result_ack',\n content: 'Stub: tool finished. Done.',\n finishReason: 'stop',\n };\n }\n\n // ── Default — plain chat echo. Includes the first 60 chars of\n // the prompt so eval suites can assert on prompt threading.\n return {\n kind: 'echo',\n content: `Stub response. Echoing your request: \"${lastUser.slice(0, 60)}\".`,\n finishReason: 'stop',\n };\n}\n\n/* ─────────────────────────── Sub-recognizers ─────────────────────────── */\n\n/** Pick a widget name keyed off the user prompt. Defaults to a generic\n * \"Note\" panel so the agent always has SOMETHING to add. */\nfunction pickWidget(prompt: string): string {\n if (/stock|ticker|nasdaq|nyse/.test(prompt)) return 'Stock Ticker';\n if (/pomodoro|timer|focus/.test(prompt)) return 'Pomodoro Timer';\n if (/calendar|schedule/.test(prompt)) return 'Calendar';\n if (/gauge|meter|metric/.test(prompt)) return 'Gauge';\n if (/dashboard/.test(prompt)) return 'Dashboard';\n if (/clock|time/.test(prompt)) return 'Clock';\n return 'Note';\n}\n\n/** Pick a tool + arguments keyed off the user prompt. Returns null if\n * no tool matches — caller falls through to text-only response. */\nfunction pickTool(prompt: string): { name: string; args: Record<string, unknown> } | null {\n if (/search\\s+for|google|find\\s+information|web\\s+search/.test(prompt)) {\n return { name: 'web_search', args: { query: extractAfter(prompt, /search\\s+for|find/) || prompt.slice(0, 80) } };\n }\n if (/write\\s+(a\\s+)?file|save\\s+to|create\\s+(a\\s+)?file/.test(prompt)) {\n return { name: 'write_file', args: { path: '/tmp/stub-output.txt', content: 'Stub provider wrote this file.' } };\n }\n if (/read\\s+(a\\s+)?file|open\\s+the\\s+file|show\\s+me\\s+the\\s+contents/.test(prompt)) {\n return { name: 'read_file', args: { path: '/tmp/stub-input.txt' } };\n }\n if (/list\\s+(the\\s+)?(files|directory|dir)|what.s\\s+in\\s+the\\s+folder/.test(prompt)) {\n return { name: 'list_dir', args: { path: '/tmp' } };\n }\n if (/fetch|http|download.+url/.test(prompt)) {\n return { name: 'web_fetch', args: { url: 'https://example.com' } };\n }\n if (/download\\s+(an?\\s+)?image|embed\\s+image/.test(prompt)) {\n return { name: 'download_image', args: { url: 'https://example.com/sample.jpg' } };\n }\n return null;\n}\n\n/** Extract the part of the prompt AFTER a matched intent verb. Used to\n * pull the search query / file name / etc. out of the prompt. */\nfunction extractAfter(prompt: string, re: RegExp): string {\n const m = prompt.match(re);\n if (!m) return '';\n const idx = (m.index ?? -1) + m[0].length;\n if (idx < 0) return '';\n return prompt.slice(idx).trim().replace(/[.?!]+$/, '').slice(0, 80);\n}\n\n/* ─────────────────────────── Provider ─────────────────────────── */\n\nexport class StubProvider extends LLMProvider {\n readonly name = 'stub';\n readonly displayName = 'TITAN Stub (CI/offline)';\n\n /**\n * Always configured. The whole point is that the stub works\n * without any credentials so CI can run end-to-end.\n */\n isConfigured(): boolean {\n return true;\n }\n\n async chat(options: ChatOptions): Promise<ChatResponse> {\n const intent = recognize(options.messages, !!options.tools?.length);\n logger.debug(COMPONENT, `chat() kind=${intent.kind} model=${options.model ?? 'stub/echo'}`);\n const promptTokens = approxTokens(options.messages);\n const completionTokens = approxTokens([{ role: 'assistant', content: intent.content }]);\n return {\n id: `stub-${Date.now()}`,\n content: intent.content,\n toolCalls: intent.toolCalls,\n usage: {\n promptTokens,\n completionTokens,\n totalTokens: promptTokens + completionTokens,\n },\n finishReason: intent.finishReason,\n model: options.model || 'stub/echo',\n };\n }\n\n async *chatStream(options: ChatOptions): AsyncGenerator<ChatStreamChunk> {\n const intent = recognize(options.messages, !!options.tools?.length);\n logger.debug(COMPONENT, `chatStream() kind=${intent.kind}`);\n // Emit text content first (if any), then any tool calls,\n // then a done marker. Matches the shape real providers use.\n if (intent.content) {\n yield { type: 'text', content: intent.content };\n }\n if (intent.toolCalls) {\n for (const tc of intent.toolCalls) {\n yield { type: 'tool_call', toolCall: tc };\n }\n }\n yield { type: 'done' };\n }\n\n async listModels(): Promise<string[]> {\n return ['stub/echo', 'stub/json', 'stub/refuse'];\n }\n\n async healthCheck(): Promise<boolean> {\n return true;\n }\n}\n\n/* ─────────────────────────── Token estimator ─────────────────────────── */\n\n/** ~4 chars per token rule-of-thumb. Not exact, but stable enough for\n * cost-budget tests that only need monotonicity. */\nfunction approxTokens(messages: ChatMessage[]): number {\n let chars = 0;\n for (const m of messages) chars += (m.content ?? '').length;\n return Math.max(1, Math.ceil(chars / 4));\n}\n\n/* ─────────────────────────── CI detection ─────────────────────────── */\n\n/**\n * Returns true when the runtime should fall back to the stub provider\n * because no real provider credentials are available. The default\n * model picker (`defaultModel.ts`) calls this to choose `stub/echo`\n * as the floor model in CI / offline environments.\n *\n * Trigger conditions (any one is enough):\n * - Explicit opt-in: `TITAN_STUB_PROVIDER=1`\n * - Standard CI signal: `CI=true` AND no Anthropic/OpenAI/Google key\n *\n * NOTE: we DELIBERATELY do NOT trigger on `VITEST=true`. Existing\n * unit tests (default-model-picker.test.ts in particular) pin the\n * \"no keys → ollama\" behavior; flipping that under VITEST would\n * break those tests. Tests that want the stub should set\n * `TITAN_STUB_PROVIDER=1` in their setup explicitly.\n */\nexport function shouldUseStubProvider(): boolean {\n if (process.env.TITAN_STUB_PROVIDER === '1') return true;\n if (process.env.CI === 'true') {\n const hasRealKey = !!(\n process.env.ANTHROPIC_API_KEY ||\n process.env.OPENAI_API_KEY ||\n process.env.GOOGLE_API_KEY ||\n process.env.OPENROUTER_API_KEY\n );\n return !hasRealKey;\n }\n return false;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../../storage/backup.js";
|
|
10
10
|
import { join, basename } from "path";
|
|
11
11
|
import { homedir } from "os";
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
12
13
|
import logger from "../../utils/logger.js";
|
|
13
14
|
const COMPONENT = "BackupSkill";
|
|
14
15
|
function titanHome() {
|
|
@@ -207,7 +208,6 @@ function schedulePath() {
|
|
|
207
208
|
}
|
|
208
209
|
function loadSchedule() {
|
|
209
210
|
try {
|
|
210
|
-
const { readFileSync, existsSync } = require("fs");
|
|
211
211
|
const p = schedulePath();
|
|
212
212
|
if (existsSync(p)) {
|
|
213
213
|
return JSON.parse(readFileSync(p, "utf-8"));
|
|
@@ -217,7 +217,6 @@ function loadSchedule() {
|
|
|
217
217
|
return { enabled: false, cron: "0 3 * * *", retention: { daily: 7, weekly: 4, monthly: 6 } };
|
|
218
218
|
}
|
|
219
219
|
function saveSchedule(s) {
|
|
220
|
-
const { writeFileSync, mkdirSync } = require("fs");
|
|
221
220
|
try {
|
|
222
221
|
mkdirSync(titanHome(), { recursive: true });
|
|
223
222
|
} catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/backup.ts"],"sourcesContent":["/**\n * TITAN — Backup Skill (U1, v5.7.2+ upgrade-safety phase)\n *\n * Why this skill exists\n * ---------------------\n * Tony's standing rule: \"make sure everyone on older versions of TITAN are\n * able to update properly without breaking their old TITAN settings and\n * functions.\" That requires:\n *\n * 1. A reliable way for users to capture their state BEFORE an upgrade.\n * 2. A reliable way to restore that state if migration fails.\n * 3. Scheduled auto-backups so even careless users have a safety net.\n *\n * `src/storage/backup.ts` already implements the file-level primitives\n * (create / list / verify / restore + SHA-256 manifest hashes + retention).\n * This skill wraps them as 5 agent-callable tools so TITAN itself can\n * back up, restore, and schedule on the user's voice command — and so the\n * migration runner (U2) can call them programmatically.\n *\n * Tools:\n * - `backup_create` — snapshot ~/.titan/ to a timestamped tar.gz\n * - `backup_list` — list available backups (newest first)\n * - `backup_verify` — SHA-256 + tar-integrity check (no apply)\n * - `backup_restore` — restore from a backup id\n * - `backup_schedule` — enable/disable cron-driven auto-backups\n *\n * Reference:\n * - src/storage/backup.ts (the primitives)\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md (U1–U6)\n */\nimport { registerSkill } from '../registry.js';\nimport {\n createBackup,\n listBackups,\n restoreBackup,\n verifyBackup,\n applyBackupRetention,\n type RetentionPolicy,\n} from '../../storage/backup.js';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'BackupSkill';\n\n// Lazy path resolution so tests can point HOME / TITAN_HOME at a temp dir\n// after this module has loaded. Mirrors src/storage/backup.ts.\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction backupDir(): string { return join(titanHome(), 'backups'); }\n\n/** Resolve a user-supplied backup id (filename, partial timestamp, or \"latest\")\n * to the absolute path of a real backup file. Returns null if no match. */\nfunction resolveBackupId(id: string | undefined): string | null {\n const all = listBackups();\n if (all.length === 0) return null;\n if (!id || id === 'latest') return all[0].path;\n // Exact filename match\n const exact = all.find(b => b.filename === id);\n if (exact) return exact.path;\n // Prefix match against filename (e.g. \"titan-backup-2026-05-10\")\n const prefix = all.find(b => b.filename.startsWith(id));\n if (prefix) return prefix.path;\n // Substring match (last resort — gives the newest match)\n const sub = all.find(b => b.filename.includes(id));\n return sub?.path ?? null;\n}\n\n// ─── Tool: backup_create ────────────────────────────────────────────\n\nconst createHandler = {\n name: 'backup_create',\n description: [\n 'Create a snapshot of the user\\'s TITAN state — `~/.titan/` config, sessions, memory, personas, dreams, auth tokens, cron jobs, recipes, autopilot config, custom skills. The snapshot is a single tar.gz with a SHA-256 manifest.',\n '',\n 'USE CASE: the user is about to make a risky change (\"backup before I try this\"), an upgrade is about to run, or the user explicitly asks for a backup.',\n 'ANTI-PATTERN: do NOT call this every chat turn — backups are cheap but not free, and clutter accumulates. Once per session at most unless the user asks.',\n '',\n 'PARAMETERS:',\n ' • label — Optional human label appended to the filename for findability (e.g. \"before-v6-upgrade\"). Sanitized to alphanumerics + dashes.',\n ' • includeWorkspace — Default true. Set false to skip the workspace/ subdir (faster, smaller).',\n ' • applyRetention — Default true. After the new backup lands, prune older backups per the daily/weekly/monthly policy.',\n '',\n 'RETURNS: \"Backup created: <filename> (<sizeKB>, <fileCount> items) — manifest hashes: <count>\" on success.',\n 'ERRORS: \"Error: ...\" when ~/.titan/ is empty or the tar invocation fails. Never partial — backup either lands whole or not at all.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n label: { type: 'string', description: 'Optional human label appended to the filename (sanitized).' },\n includeWorkspace: { type: 'boolean', description: 'Include workspace/ subdir (default true).' },\n applyRetention: { type: 'boolean', description: 'Prune older backups per the daily/weekly/monthly policy after this backup lands (default true).' },\n },\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const includeWorkspace = (args.includeWorkspace ?? true) as boolean;\n const applyRetention = (args.applyRetention ?? true) as boolean;\n const label = typeof args.label === 'string'\n ? args.label.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 40)\n : '';\n\n try {\n const info = await createBackup({ includeWorkspace });\n // If a label was provided, rename the file to embed it for findability.\n // Keep the rename idempotent and silent on conflict.\n let finalPath = info.path;\n let finalName = info.filename;\n if (label) {\n const labelled = info.filename.replace(/\\.tar\\.gz$/, `-${label}.tar.gz`);\n const labelledPath = join(backupDir(), labelled);\n try {\n const { renameSync } = await import('fs');\n renameSync(info.path, labelledPath);\n finalPath = labelledPath;\n finalName = labelled;\n } catch (err) {\n logger.warn(COMPONENT, `Could not apply label \"${label}\": ${(err as Error).message}`);\n }\n }\n\n if (applyRetention) {\n applyBackupRetention();\n }\n\n const sizeKB = (info.sizeBytes / 1024).toFixed(1);\n const hashCount = info.manifest?.files.filter(f => f.hash).length ?? 0;\n const fileCount = info.manifest?.files.length ?? 0;\n return `Backup created: ${finalName} (${sizeKB} KB, ${fileCount} items) — manifest hashes: ${hashCount}\\nPath: ${finalPath}`;\n } catch (err) {\n return `Error: backup_create failed — ${(err as Error).message}`;\n }\n },\n};\n\n// ─── Tool: backup_list ──────────────────────────────────────────────\n\nconst listHandler = {\n name: 'backup_list',\n description: [\n 'List the user\\'s TITAN backups, newest first. Use this before `backup_restore` to find the id you want.',\n '',\n 'USE CASE: the user asks \"what backups do I have?\", or you need a backup id for restore.',\n '',\n 'PARAMETERS:',\n ' • limit — Cap on number of entries returned (default 20).',\n '',\n 'RETURNS: A markdown table with filename, createdAt, size. Empty-state message when no backups exist.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n limit: { type: 'number', description: 'Max entries to return (default 20).' },\n },\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const limit = Math.max(1, Math.min(100, (args.limit as number) ?? 20));\n const all = listBackups().slice(0, limit);\n if (all.length === 0) {\n return 'No backups found. Run `backup_create` to make your first one.';\n }\n const lines = [\n '| # | Filename | Created | Size |',\n '|---|----------|---------|------|',\n ];\n for (let i = 0; i < all.length; i++) {\n const b = all[i];\n const sizeKB = (b.sizeBytes / 1024).toFixed(1);\n lines.push(`| ${i + 1} | \\`${b.filename}\\` | ${b.createdAt} | ${sizeKB} KB |`);\n }\n return lines.join('\\n');\n },\n};\n\n// ─── Tool: backup_verify ────────────────────────────────────────────\n\nconst verifyHandler = {\n name: 'backup_verify',\n description: [\n 'Verify a backup\\'s integrity (tar structure + SHA-256 manifest) without restoring. Returns a pass/fail with details.',\n '',\n 'USE CASE: confirm a backup is healthy before relying on it for a restore.',\n '',\n 'PARAMETERS:',\n ' • id — Backup id: filename, prefix (e.g. \"titan-backup-2026-05-10\"), or \"latest\" for the newest backup. Default \"latest\".',\n '',\n 'RETURNS: \"✓ Valid: <filename> (TITAN <ver>, <N> files, <hashCount> with hashes)\" on pass.',\n 'ERRORS: \"✗ Invalid: <reason>\" if the archive is corrupt or the manifest mismatches.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Backup id (filename, prefix, or \"latest\").' },\n },\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id : 'latest';\n const path = resolveBackupId(id);\n if (!path) {\n return `Error: no backup matches \"${id}\". Run \\`backup_list\\` to see available backups.`;\n }\n const r = await verifyBackup(path);\n if (!r.valid) {\n return `✗ Invalid: ${basename(path)} — ${r.error || 'unknown verification failure'}`;\n }\n if (r.manifest) {\n const hashCount = r.manifest.files.filter(f => f.hash).length;\n return `✓ Valid: ${basename(path)} (TITAN ${r.manifest.titanVersion}, ${r.manifest.files.length} files, ${hashCount} with hashes)`;\n }\n return `✓ Valid: ${basename(path)} (legacy backup without manifest — restore-safe but no integrity check)`;\n },\n};\n\n// ─── Tool: backup_restore ───────────────────────────────────────────\n\nconst restoreHandler = {\n name: 'backup_restore',\n description: [\n 'Restore TITAN state from a backup. ⚠️ This OVERWRITES current ~/.titan/ content. The agent should verify the backup first with `backup_verify` and confirm the user\\'s intent.',\n '',\n 'USE CASE: user wants to roll back to a known-good state after a migration or upgrade went wrong.',\n 'ANTI-PATTERN: do NOT auto-restore. Always have the user confirm — restore is destructive.',\n '',\n 'PARAMETERS:',\n ' • id — Backup id (filename, prefix, or \"latest\"). Required.',\n ' • skipVerify — Skip the pre-restore integrity check (default false). Use only when verify is known broken but you trust the archive.',\n '',\n 'RETURNS: \"✓ Restored from <filename> (<N> items)\" on success.',\n 'ERRORS: \"Error: ...\" when the backup is corrupt or restore aborts mid-flight.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Backup id (filename, prefix, or \"latest\").' },\n skipVerify: { type: 'boolean', description: 'Skip the pre-restore integrity check (default false).' },\n },\n required: ['id'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id : '';\n const skipVerify = Boolean(args.skipVerify);\n if (!id) return 'Error: \"id\" is required. Run `backup_list` to find one.';\n\n const path = resolveBackupId(id);\n if (!path) return `Error: no backup matches \"${id}\".`;\n\n if (!skipVerify) {\n const r = await verifyBackup(path);\n if (!r.valid) {\n return `Error: cannot restore — backup failed integrity check (${r.error}). Pass skipVerify:true if you trust this archive anyway.`;\n }\n }\n try {\n const result = await restoreBackup(path);\n return `✓ Restored from ${basename(path)} (${result.restored.length} items)`;\n } catch (err) {\n return `Error: backup_restore failed — ${(err as Error).message}`;\n }\n },\n};\n\n// ─── Tool: backup_schedule ──────────────────────────────────────────\n\ninterface ScheduleConfig {\n enabled: boolean;\n cron: string; // e.g. \"0 3 * * *\" for daily 3am\n retention: RetentionPolicy;\n nextRunAt?: string;\n}\n\nfunction schedulePath(): string { return join(titanHome(), 'backup-schedule.json'); }\n\nfunction loadSchedule(): ScheduleConfig {\n try {\n const { readFileSync, existsSync } = require('fs') as typeof import('fs');\n const p = schedulePath();\n if (existsSync(p)) {\n return JSON.parse(readFileSync(p, 'utf-8')) as ScheduleConfig;\n }\n } catch { /* fall through */ }\n return { enabled: false, cron: '0 3 * * *', retention: { daily: 7, weekly: 4, monthly: 6 } };\n}\n\nfunction saveSchedule(s: ScheduleConfig): void {\n const { writeFileSync, mkdirSync } = require('fs') as typeof import('fs');\n try { mkdirSync(titanHome(), { recursive: true }); } catch { /* exists */ }\n writeFileSync(schedulePath(), JSON.stringify(s, null, 2));\n}\n\nconst scheduleHandler = {\n name: 'backup_schedule',\n description: [\n 'Configure scheduled auto-backups. Reads/writes `~/.titan/backup-schedule.json`. The actual cron firing is wired through TITAN\\'s cron skill on next gateway boot.',\n '',\n 'USE CASE: user says \"back up my TITAN every night\", \"stop auto-backups\", \"keep 30 days of dailies\".',\n '',\n 'PARAMETERS:',\n ' • enabled — Required. true to turn auto-backup on, false off.',\n ' • cron — Cron expression. Default \"0 3 * * *\" (daily at 3am local).',\n ' • daily / weekly / monthly — Retention counts (defaults 7 / 4 / 6).',\n '',\n 'RETURNS: a summary of the saved schedule.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n enabled: { type: 'boolean', description: 'Turn auto-backup on/off (required).' },\n cron: { type: 'string', description: 'Cron expression (default \"0 3 * * *\").' },\n daily: { type: 'number', description: 'Daily retention count (default 7).' },\n weekly: { type: 'number', description: 'Weekly retention count (default 4).' },\n monthly: { type: 'number', description: 'Monthly retention count (default 6).' },\n },\n required: ['enabled'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const current = loadSchedule();\n const next: ScheduleConfig = {\n enabled: Boolean(args.enabled),\n cron: typeof args.cron === 'string' && args.cron.trim() ? args.cron.trim() : current.cron,\n retention: {\n daily: (args.daily as number) ?? current.retention.daily ?? 7,\n weekly: (args.weekly as number) ?? current.retention.weekly ?? 4,\n monthly: (args.monthly as number) ?? current.retention.monthly ?? 6,\n },\n };\n saveSchedule(next);\n logger.info(COMPONENT, `backup_schedule updated — enabled=${next.enabled}, cron=\"${next.cron}\", retention=${JSON.stringify(next.retention)}`);\n return [\n `Backup schedule saved:`,\n ` enabled: ${next.enabled}`,\n ` cron: ${next.cron}`,\n ` retention: ${next.retention.daily} daily, ${next.retention.weekly} weekly, ${next.retention.monthly} monthly`,\n ``,\n next.enabled\n ? `Auto-backups will run on the next gateway boot. Until then, you can call \\`backup_create\\` manually.`\n : `Auto-backups are disabled. Manual \\`backup_create\\` calls still work.`,\n ].join('\\n');\n },\n};\n\n// ─── Registration ──────────────────────────────────────────────────\n\nexport function registerBackupSkill(): void {\n const meta = {\n name: 'backup',\n description: 'TITAN state backup + restore + scheduled snapshots. Protects user config, sessions, memory, personas, and custom skills across upgrades.',\n version: '1.0.0',\n source: 'bundled' as const,\n enabled: true,\n };\n registerSkill(meta, createHandler);\n registerSkill(meta, listHandler);\n registerSkill(meta, verifyHandler);\n registerSkill(meta, restoreHandler);\n registerSkill(meta, scheduleHandler);\n}\n\n// Exported for tests.\nexport const __test__ = {\n resolveBackupId,\n loadSchedule,\n saveSchedule,\n schedulePath,\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/backup.ts"],"sourcesContent":["/**\n * TITAN — Backup Skill (U1, v5.7.2+ upgrade-safety phase)\n *\n * Why this skill exists\n * ---------------------\n * Tony's standing rule: \"make sure everyone on older versions of TITAN are\n * able to update properly without breaking their old TITAN settings and\n * functions.\" That requires:\n *\n * 1. A reliable way for users to capture their state BEFORE an upgrade.\n * 2. A reliable way to restore that state if migration fails.\n * 3. Scheduled auto-backups so even careless users have a safety net.\n *\n * `src/storage/backup.ts` already implements the file-level primitives\n * (create / list / verify / restore + SHA-256 manifest hashes + retention).\n * This skill wraps them as 5 agent-callable tools so TITAN itself can\n * back up, restore, and schedule on the user's voice command — and so the\n * migration runner (U2) can call them programmatically.\n *\n * Tools:\n * - `backup_create` — snapshot ~/.titan/ to a timestamped tar.gz\n * - `backup_list` — list available backups (newest first)\n * - `backup_verify` — SHA-256 + tar-integrity check (no apply)\n * - `backup_restore` — restore from a backup id\n * - `backup_schedule` — enable/disable cron-driven auto-backups\n *\n * Reference:\n * - src/storage/backup.ts (the primitives)\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md (U1–U6)\n */\nimport { registerSkill } from '../registry.js';\nimport {\n createBackup,\n listBackups,\n restoreBackup,\n verifyBackup,\n applyBackupRetention,\n type RetentionPolicy,\n} from '../../storage/backup.js';\nimport { join, basename } from 'path';\nimport { homedir } from 'os';\nimport { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';\nimport logger from '../../utils/logger.js';\n\nconst COMPONENT = 'BackupSkill';\n\n// Lazy path resolution so tests can point HOME / TITAN_HOME at a temp dir\n// after this module has loaded. Mirrors src/storage/backup.ts.\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction backupDir(): string { return join(titanHome(), 'backups'); }\n\n/** Resolve a user-supplied backup id (filename, partial timestamp, or \"latest\")\n * to the absolute path of a real backup file. Returns null if no match. */\nfunction resolveBackupId(id: string | undefined): string | null {\n const all = listBackups();\n if (all.length === 0) return null;\n if (!id || id === 'latest') return all[0].path;\n // Exact filename match\n const exact = all.find(b => b.filename === id);\n if (exact) return exact.path;\n // Prefix match against filename (e.g. \"titan-backup-2026-05-10\")\n const prefix = all.find(b => b.filename.startsWith(id));\n if (prefix) return prefix.path;\n // Substring match (last resort — gives the newest match)\n const sub = all.find(b => b.filename.includes(id));\n return sub?.path ?? null;\n}\n\n// ─── Tool: backup_create ────────────────────────────────────────────\n\nconst createHandler = {\n name: 'backup_create',\n description: [\n 'Create a snapshot of the user\\'s TITAN state — `~/.titan/` config, sessions, memory, personas, dreams, auth tokens, cron jobs, recipes, autopilot config, custom skills. The snapshot is a single tar.gz with a SHA-256 manifest.',\n '',\n 'USE CASE: the user is about to make a risky change (\"backup before I try this\"), an upgrade is about to run, or the user explicitly asks for a backup.',\n 'ANTI-PATTERN: do NOT call this every chat turn — backups are cheap but not free, and clutter accumulates. Once per session at most unless the user asks.',\n '',\n 'PARAMETERS:',\n ' • label — Optional human label appended to the filename for findability (e.g. \"before-v6-upgrade\"). Sanitized to alphanumerics + dashes.',\n ' • includeWorkspace — Default true. Set false to skip the workspace/ subdir (faster, smaller).',\n ' • applyRetention — Default true. After the new backup lands, prune older backups per the daily/weekly/monthly policy.',\n '',\n 'RETURNS: \"Backup created: <filename> (<sizeKB>, <fileCount> items) — manifest hashes: <count>\" on success.',\n 'ERRORS: \"Error: ...\" when ~/.titan/ is empty or the tar invocation fails. Never partial — backup either lands whole or not at all.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n label: { type: 'string', description: 'Optional human label appended to the filename (sanitized).' },\n includeWorkspace: { type: 'boolean', description: 'Include workspace/ subdir (default true).' },\n applyRetention: { type: 'boolean', description: 'Prune older backups per the daily/weekly/monthly policy after this backup lands (default true).' },\n },\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const includeWorkspace = (args.includeWorkspace ?? true) as boolean;\n const applyRetention = (args.applyRetention ?? true) as boolean;\n const label = typeof args.label === 'string'\n ? args.label.replace(/[^a-zA-Z0-9-]/g, '').slice(0, 40)\n : '';\n\n try {\n const info = await createBackup({ includeWorkspace });\n // If a label was provided, rename the file to embed it for findability.\n // Keep the rename idempotent and silent on conflict.\n let finalPath = info.path;\n let finalName = info.filename;\n if (label) {\n const labelled = info.filename.replace(/\\.tar\\.gz$/, `-${label}.tar.gz`);\n const labelledPath = join(backupDir(), labelled);\n try {\n const { renameSync } = await import('fs');\n renameSync(info.path, labelledPath);\n finalPath = labelledPath;\n finalName = labelled;\n } catch (err) {\n logger.warn(COMPONENT, `Could not apply label \"${label}\": ${(err as Error).message}`);\n }\n }\n\n if (applyRetention) {\n applyBackupRetention();\n }\n\n const sizeKB = (info.sizeBytes / 1024).toFixed(1);\n const hashCount = info.manifest?.files.filter(f => f.hash).length ?? 0;\n const fileCount = info.manifest?.files.length ?? 0;\n return `Backup created: ${finalName} (${sizeKB} KB, ${fileCount} items) — manifest hashes: ${hashCount}\\nPath: ${finalPath}`;\n } catch (err) {\n return `Error: backup_create failed — ${(err as Error).message}`;\n }\n },\n};\n\n// ─── Tool: backup_list ──────────────────────────────────────────────\n\nconst listHandler = {\n name: 'backup_list',\n description: [\n 'List the user\\'s TITAN backups, newest first. Use this before `backup_restore` to find the id you want.',\n '',\n 'USE CASE: the user asks \"what backups do I have?\", or you need a backup id for restore.',\n '',\n 'PARAMETERS:',\n ' • limit — Cap on number of entries returned (default 20).',\n '',\n 'RETURNS: A markdown table with filename, createdAt, size. Empty-state message when no backups exist.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n limit: { type: 'number', description: 'Max entries to return (default 20).' },\n },\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const limit = Math.max(1, Math.min(100, (args.limit as number) ?? 20));\n const all = listBackups().slice(0, limit);\n if (all.length === 0) {\n return 'No backups found. Run `backup_create` to make your first one.';\n }\n const lines = [\n '| # | Filename | Created | Size |',\n '|---|----------|---------|------|',\n ];\n for (let i = 0; i < all.length; i++) {\n const b = all[i];\n const sizeKB = (b.sizeBytes / 1024).toFixed(1);\n lines.push(`| ${i + 1} | \\`${b.filename}\\` | ${b.createdAt} | ${sizeKB} KB |`);\n }\n return lines.join('\\n');\n },\n};\n\n// ─── Tool: backup_verify ────────────────────────────────────────────\n\nconst verifyHandler = {\n name: 'backup_verify',\n description: [\n 'Verify a backup\\'s integrity (tar structure + SHA-256 manifest) without restoring. Returns a pass/fail with details.',\n '',\n 'USE CASE: confirm a backup is healthy before relying on it for a restore.',\n '',\n 'PARAMETERS:',\n ' • id — Backup id: filename, prefix (e.g. \"titan-backup-2026-05-10\"), or \"latest\" for the newest backup. Default \"latest\".',\n '',\n 'RETURNS: \"✓ Valid: <filename> (TITAN <ver>, <N> files, <hashCount> with hashes)\" on pass.',\n 'ERRORS: \"✗ Invalid: <reason>\" if the archive is corrupt or the manifest mismatches.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Backup id (filename, prefix, or \"latest\").' },\n },\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id : 'latest';\n const path = resolveBackupId(id);\n if (!path) {\n return `Error: no backup matches \"${id}\". Run \\`backup_list\\` to see available backups.`;\n }\n const r = await verifyBackup(path);\n if (!r.valid) {\n return `✗ Invalid: ${basename(path)} — ${r.error || 'unknown verification failure'}`;\n }\n if (r.manifest) {\n const hashCount = r.manifest.files.filter(f => f.hash).length;\n return `✓ Valid: ${basename(path)} (TITAN ${r.manifest.titanVersion}, ${r.manifest.files.length} files, ${hashCount} with hashes)`;\n }\n return `✓ Valid: ${basename(path)} (legacy backup without manifest — restore-safe but no integrity check)`;\n },\n};\n\n// ─── Tool: backup_restore ───────────────────────────────────────────\n\nconst restoreHandler = {\n name: 'backup_restore',\n description: [\n 'Restore TITAN state from a backup. ⚠️ This OVERWRITES current ~/.titan/ content. The agent should verify the backup first with `backup_verify` and confirm the user\\'s intent.',\n '',\n 'USE CASE: user wants to roll back to a known-good state after a migration or upgrade went wrong.',\n 'ANTI-PATTERN: do NOT auto-restore. Always have the user confirm — restore is destructive.',\n '',\n 'PARAMETERS:',\n ' • id — Backup id (filename, prefix, or \"latest\"). Required.',\n ' • skipVerify — Skip the pre-restore integrity check (default false). Use only when verify is known broken but you trust the archive.',\n '',\n 'RETURNS: \"✓ Restored from <filename> (<N> items)\" on success.',\n 'ERRORS: \"Error: ...\" when the backup is corrupt or restore aborts mid-flight.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Backup id (filename, prefix, or \"latest\").' },\n skipVerify: { type: 'boolean', description: 'Skip the pre-restore integrity check (default false).' },\n },\n required: ['id'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const id = typeof args.id === 'string' ? args.id : '';\n const skipVerify = Boolean(args.skipVerify);\n if (!id) return 'Error: \"id\" is required. Run `backup_list` to find one.';\n\n const path = resolveBackupId(id);\n if (!path) return `Error: no backup matches \"${id}\".`;\n\n if (!skipVerify) {\n const r = await verifyBackup(path);\n if (!r.valid) {\n return `Error: cannot restore — backup failed integrity check (${r.error}). Pass skipVerify:true if you trust this archive anyway.`;\n }\n }\n try {\n const result = await restoreBackup(path);\n return `✓ Restored from ${basename(path)} (${result.restored.length} items)`;\n } catch (err) {\n return `Error: backup_restore failed — ${(err as Error).message}`;\n }\n },\n};\n\n// ─── Tool: backup_schedule ──────────────────────────────────────────\n\ninterface ScheduleConfig {\n enabled: boolean;\n cron: string; // e.g. \"0 3 * * *\" for daily 3am\n retention: RetentionPolicy;\n nextRunAt?: string;\n}\n\nfunction schedulePath(): string { return join(titanHome(), 'backup-schedule.json'); }\n\nfunction loadSchedule(): ScheduleConfig {\n try {\n const p = schedulePath();\n if (existsSync(p)) {\n return JSON.parse(readFileSync(p, 'utf-8')) as ScheduleConfig;\n }\n } catch { /* fall through */ }\n return { enabled: false, cron: '0 3 * * *', retention: { daily: 7, weekly: 4, monthly: 6 } };\n}\n\nfunction saveSchedule(s: ScheduleConfig): void {\n try { mkdirSync(titanHome(), { recursive: true }); } catch { /* exists */ }\n writeFileSync(schedulePath(), JSON.stringify(s, null, 2));\n}\n\nconst scheduleHandler = {\n name: 'backup_schedule',\n description: [\n 'Configure scheduled auto-backups. Reads/writes `~/.titan/backup-schedule.json`. The actual cron firing is wired through TITAN\\'s cron skill on next gateway boot.',\n '',\n 'USE CASE: user says \"back up my TITAN every night\", \"stop auto-backups\", \"keep 30 days of dailies\".',\n '',\n 'PARAMETERS:',\n ' • enabled — Required. true to turn auto-backup on, false off.',\n ' • cron — Cron expression. Default \"0 3 * * *\" (daily at 3am local).',\n ' • daily / weekly / monthly — Retention counts (defaults 7 / 4 / 6).',\n '',\n 'RETURNS: a summary of the saved schedule.',\n ].join('\\n'),\n parameters: {\n type: 'object',\n properties: {\n enabled: { type: 'boolean', description: 'Turn auto-backup on/off (required).' },\n cron: { type: 'string', description: 'Cron expression (default \"0 3 * * *\").' },\n daily: { type: 'number', description: 'Daily retention count (default 7).' },\n weekly: { type: 'number', description: 'Weekly retention count (default 4).' },\n monthly: { type: 'number', description: 'Monthly retention count (default 6).' },\n },\n required: ['enabled'],\n },\n execute: async (args: Record<string, unknown>): Promise<string> => {\n const current = loadSchedule();\n const next: ScheduleConfig = {\n enabled: Boolean(args.enabled),\n cron: typeof args.cron === 'string' && args.cron.trim() ? args.cron.trim() : current.cron,\n retention: {\n daily: (args.daily as number) ?? current.retention.daily ?? 7,\n weekly: (args.weekly as number) ?? current.retention.weekly ?? 4,\n monthly: (args.monthly as number) ?? current.retention.monthly ?? 6,\n },\n };\n saveSchedule(next);\n logger.info(COMPONENT, `backup_schedule updated — enabled=${next.enabled}, cron=\"${next.cron}\", retention=${JSON.stringify(next.retention)}`);\n return [\n `Backup schedule saved:`,\n ` enabled: ${next.enabled}`,\n ` cron: ${next.cron}`,\n ` retention: ${next.retention.daily} daily, ${next.retention.weekly} weekly, ${next.retention.monthly} monthly`,\n ``,\n next.enabled\n ? `Auto-backups will run on the next gateway boot. Until then, you can call \\`backup_create\\` manually.`\n : `Auto-backups are disabled. Manual \\`backup_create\\` calls still work.`,\n ].join('\\n');\n },\n};\n\n// ─── Registration ──────────────────────────────────────────────────\n\nexport function registerBackupSkill(): void {\n const meta = {\n name: 'backup',\n description: 'TITAN state backup + restore + scheduled snapshots. Protects user config, sessions, memory, personas, and custom skills across upgrades.',\n version: '1.0.0',\n source: 'bundled' as const,\n enabled: true,\n };\n registerSkill(meta, createHandler);\n registerSkill(meta, listHandler);\n registerSkill(meta, verifyHandler);\n registerSkill(meta, restoreHandler);\n registerSkill(meta, scheduleHandler);\n}\n\n// Exported for tests.\nexport const __test__ = {\n resolveBackupId,\n loadSchedule,\n saveSchedule,\n schedulePath,\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { registerToolContract } from "../../agent/toolContract.js";
|
|
4
|
+
const WRITE_FILE_CONTRACT = {
|
|
5
|
+
name: "write_file",
|
|
6
|
+
summary: "Write content to a file. Overwrites if it exists. Path must be under home or /tmp.",
|
|
7
|
+
input: z.object({
|
|
8
|
+
path: z.string().min(1, "path is required"),
|
|
9
|
+
content: z.string()
|
|
10
|
+
// empty content is valid — touching a file
|
|
11
|
+
}).passthrough(),
|
|
12
|
+
sideEffects: ["write"],
|
|
13
|
+
// beta.17 — writes are 'moderate' (was 'safe'). Codex P1 #2: writes
|
|
14
|
+
// can overwrite files and mutate user state. The auto-mode classifier
|
|
15
|
+
// maps 'moderate' to 'notify' under standard policy (runs but logs)
|
|
16
|
+
// and 'gate' under 'paranoid'. Path-aware scoping (allowing /tmp/*
|
|
17
|
+
// and ~/.titan/workspace/* writes to auto-run) is a follow-up — see
|
|
18
|
+
// claude_notes in .ai_bridge.json.
|
|
19
|
+
riskLevel: "moderate",
|
|
20
|
+
exampleCalls: [
|
|
21
|
+
{
|
|
22
|
+
description: "write a small note",
|
|
23
|
+
args: { path: "/tmp/note.txt", content: "hello world\n" }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
description: "write an HTML report in the home dir",
|
|
27
|
+
args: { path: "~/reports/q4.html", content: "<html>...</html>" }
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
};
|
|
31
|
+
const READ_FILE_CONTRACT = {
|
|
32
|
+
name: "read_file",
|
|
33
|
+
summary: "Read a file from disk. Optional startLine/endLine for partial reads.",
|
|
34
|
+
// beta.17 (Codex P1): matched to the actual filesystem.ts skill
|
|
35
|
+
// shape — read_file accepts {path, startLine, endLine}. The
|
|
36
|
+
// byteOffset/byteLimit fields the original contract had don't
|
|
37
|
+
// exist on the skill; Zod was silently stripping them.
|
|
38
|
+
// .passthrough() preserves any extras so a future skill addition
|
|
39
|
+
// doesn't silently drop before we catch up the contract.
|
|
40
|
+
input: z.object({
|
|
41
|
+
path: z.string().min(1),
|
|
42
|
+
startLine: z.number().int().nonnegative().optional(),
|
|
43
|
+
endLine: z.number().int().nonnegative().optional()
|
|
44
|
+
}).passthrough(),
|
|
45
|
+
sideEffects: ["read"],
|
|
46
|
+
riskLevel: "safe",
|
|
47
|
+
exampleCalls: [
|
|
48
|
+
{
|
|
49
|
+
description: "read a whole file",
|
|
50
|
+
args: { path: "/tmp/note.txt" }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
description: "read lines 100-200 of a file",
|
|
54
|
+
args: { path: "/var/log/titan.log", startLine: 100, endLine: 200 }
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
};
|
|
58
|
+
const EDIT_FILE_CONTRACT = {
|
|
59
|
+
name: "edit_file",
|
|
60
|
+
summary: "Find-and-replace exactly one occurrence of target with replacement.",
|
|
61
|
+
input: z.object({
|
|
62
|
+
path: z.string().min(1),
|
|
63
|
+
target: z.string().min(1, "target must not be empty"),
|
|
64
|
+
replacement: z.string()
|
|
65
|
+
}).passthrough(),
|
|
66
|
+
sideEffects: ["write"],
|
|
67
|
+
// beta.17 — see WRITE_FILE_CONTRACT comment. Writes are 'moderate'.
|
|
68
|
+
riskLevel: "moderate",
|
|
69
|
+
exampleCalls: [
|
|
70
|
+
{
|
|
71
|
+
description: "fix a typo",
|
|
72
|
+
args: { path: "/tmp/note.txt", target: "teh", replacement: "the" }
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
const APPEND_FILE_CONTRACT = {
|
|
77
|
+
name: "append_file",
|
|
78
|
+
summary: "Append content to the end of a file. Creates the file if missing.",
|
|
79
|
+
input: z.object({
|
|
80
|
+
path: z.string().min(1),
|
|
81
|
+
content: z.string().min(1, "content must not be empty")
|
|
82
|
+
}).passthrough(),
|
|
83
|
+
sideEffects: ["write"],
|
|
84
|
+
// beta.17 — see WRITE_FILE_CONTRACT comment. Writes are 'moderate'.
|
|
85
|
+
riskLevel: "moderate",
|
|
86
|
+
exampleCalls: [
|
|
87
|
+
{
|
|
88
|
+
description: "append a log line",
|
|
89
|
+
args: { path: "/tmp/build.log", content: "INFO: build started\n" }
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
};
|
|
93
|
+
const LIST_DIR_CONTRACT = {
|
|
94
|
+
name: "list_dir",
|
|
95
|
+
summary: "List the contents of a directory. Optionally recursive.",
|
|
96
|
+
// beta.17 (Codex P1): matched to actual filesystem.ts skill shape.
|
|
97
|
+
// The skill accepts {path, recursive}; the original contract added
|
|
98
|
+
// a `pattern` field the skill doesn't read. Dropped.
|
|
99
|
+
input: z.object({
|
|
100
|
+
path: z.string().min(1),
|
|
101
|
+
recursive: z.boolean().optional()
|
|
102
|
+
}).passthrough(),
|
|
103
|
+
sideEffects: ["read"],
|
|
104
|
+
riskLevel: "safe",
|
|
105
|
+
exampleCalls: [
|
|
106
|
+
{
|
|
107
|
+
description: "list current directory contents",
|
|
108
|
+
args: { path: "." }
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
description: "list a tree recursively",
|
|
112
|
+
args: { path: "src", recursive: true }
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
const WEB_SEARCH_CONTRACT = {
|
|
117
|
+
name: "web_search",
|
|
118
|
+
summary: "Search the public web. Returns ranked results with titles, URLs, and snippets.",
|
|
119
|
+
// beta.17 (Codex P1): the actual web_search.ts skill uses `maxResults`,
|
|
120
|
+
// not `numResults`. The original contract silently dropped the
|
|
121
|
+
// caller's count preference because Zod stripped the unknown
|
|
122
|
+
// `maxResults` and the skill only saw `query`. Field renamed to
|
|
123
|
+
// match; .passthrough() guards against future skill additions.
|
|
124
|
+
input: z.object({
|
|
125
|
+
query: z.string().min(1, "query must not be empty"),
|
|
126
|
+
maxResults: z.number().int().min(1).max(50).optional()
|
|
127
|
+
}).passthrough(),
|
|
128
|
+
sideEffects: ["network", "read"],
|
|
129
|
+
riskLevel: "moderate",
|
|
130
|
+
exampleCalls: [
|
|
131
|
+
{
|
|
132
|
+
description: "a simple lookup",
|
|
133
|
+
args: { query: "TypeScript discriminated union" }
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
description: "a focused lookup with explicit count",
|
|
137
|
+
args: { query: "discriminated unions", maxResults: 5 }
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
};
|
|
141
|
+
const WEB_FETCH_CONTRACT = {
|
|
142
|
+
name: "web_fetch",
|
|
143
|
+
summary: "Fetch a URL over HTTP(S) and return its content as markdown or text. Internal/private network addresses are blocked.",
|
|
144
|
+
// beta.17 (Codex P1): the actual web_fetch.ts skill accepts
|
|
145
|
+
// {url, extractMode: 'markdown'|'text', maxChars}, NOT
|
|
146
|
+
// {url, method, headers, body}. The original contract advertised a
|
|
147
|
+
// generic HTTP-call shape the skill never had; LLMs passing
|
|
148
|
+
// method/headers/body were getting them silently stripped.
|
|
149
|
+
// Schema now mirrors the actual skill. .passthrough() preserves
|
|
150
|
+
// any future additions.
|
|
151
|
+
input: z.object({
|
|
152
|
+
url: z.string().url("url must be a valid http(s) URL"),
|
|
153
|
+
extractMode: z.enum(["markdown", "text"]).optional(),
|
|
154
|
+
maxChars: z.number().int().positive().max(2e5).optional()
|
|
155
|
+
}).passthrough(),
|
|
156
|
+
sideEffects: ["network", "read"],
|
|
157
|
+
riskLevel: "moderate",
|
|
158
|
+
exampleCalls: [
|
|
159
|
+
{
|
|
160
|
+
description: "fetch an article as markdown",
|
|
161
|
+
args: { url: "https://example.com/article" }
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
description: "fetch as plain text with a tighter cap",
|
|
165
|
+
args: { url: "https://example.com/long-page", extractMode: "text", maxChars: 8e3 }
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
};
|
|
169
|
+
const DOWNLOAD_IMAGE_CONTRACT = {
|
|
170
|
+
name: "download_image",
|
|
171
|
+
summary: "Download an image and return a tdi:// reference token for embedding in HTML.",
|
|
172
|
+
// beta.17 (Codex P1 audit): download_image.ts skill takes {url} only;
|
|
173
|
+
// contract matches. .passthrough() added defensively in case the
|
|
174
|
+
// skill grows options later.
|
|
175
|
+
input: z.object({
|
|
176
|
+
url: z.string().url("url must be a valid http(s) URL")
|
|
177
|
+
}).passthrough(),
|
|
178
|
+
sideEffects: ["network", "write"],
|
|
179
|
+
riskLevel: "moderate",
|
|
180
|
+
exampleCalls: [
|
|
181
|
+
{
|
|
182
|
+
description: "download a public image",
|
|
183
|
+
args: { url: "https://upload.wikimedia.org/wikipedia/commons/example.jpg" }
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
};
|
|
187
|
+
const SHELL_CONTRACT = {
|
|
188
|
+
name: "shell",
|
|
189
|
+
summary: "Execute a shell command. Destructive \u2014 guarded by the sandbox + approval gate.",
|
|
190
|
+
// beta.17 (Codex P1): matched to actual shell.ts skill shape. The
|
|
191
|
+
// skill uses `timeout` (not `timeoutMs`) and also accepts
|
|
192
|
+
// `background` + `verify_port` for dev-server launches. The
|
|
193
|
+
// original contract used the wrong name + missed the background
|
|
194
|
+
// path entirely. Zod was silently stripping `background` /
|
|
195
|
+
// `verify_port` whenever the LLM tried to launch a background
|
|
196
|
+
// process — the agent would then complain that the dev server
|
|
197
|
+
// "didn't start" because shell took the command synchronously.
|
|
198
|
+
input: z.object({
|
|
199
|
+
command: z.string().min(1, "command must not be empty"),
|
|
200
|
+
cwd: z.string().optional(),
|
|
201
|
+
timeout: z.number().int().positive().max(6e5).optional(),
|
|
202
|
+
background: z.boolean().optional(),
|
|
203
|
+
verify_port: z.number().int().min(1).max(65535).optional()
|
|
204
|
+
}).passthrough(),
|
|
205
|
+
sideEffects: ["destructive"],
|
|
206
|
+
riskLevel: "high",
|
|
207
|
+
exampleCalls: [
|
|
208
|
+
{
|
|
209
|
+
description: "list files",
|
|
210
|
+
args: { command: "ls -la" }
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
description: "check disk usage in a specific dir",
|
|
214
|
+
args: { command: "du -sh", cwd: "/var/log" }
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
description: "launch a dev server in the background, wait for port 3000",
|
|
218
|
+
args: { command: "npm run dev", background: true, verify_port: 3e3 }
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
};
|
|
222
|
+
const CANONICAL_CONTRACTS = [
|
|
223
|
+
WRITE_FILE_CONTRACT,
|
|
224
|
+
READ_FILE_CONTRACT,
|
|
225
|
+
EDIT_FILE_CONTRACT,
|
|
226
|
+
APPEND_FILE_CONTRACT,
|
|
227
|
+
LIST_DIR_CONTRACT,
|
|
228
|
+
WEB_SEARCH_CONTRACT,
|
|
229
|
+
WEB_FETCH_CONTRACT,
|
|
230
|
+
DOWNLOAD_IMAGE_CONTRACT,
|
|
231
|
+
SHELL_CONTRACT
|
|
232
|
+
];
|
|
233
|
+
function registerCanonicalContracts() {
|
|
234
|
+
for (const c of CANONICAL_CONTRACTS) {
|
|
235
|
+
registerToolContract(c);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function listCanonicalContracts() {
|
|
239
|
+
return [...CANONICAL_CONTRACTS];
|
|
240
|
+
}
|
|
241
|
+
export {
|
|
242
|
+
APPEND_FILE_CONTRACT,
|
|
243
|
+
DOWNLOAD_IMAGE_CONTRACT,
|
|
244
|
+
EDIT_FILE_CONTRACT,
|
|
245
|
+
LIST_DIR_CONTRACT,
|
|
246
|
+
READ_FILE_CONTRACT,
|
|
247
|
+
SHELL_CONTRACT,
|
|
248
|
+
WEB_FETCH_CONTRACT,
|
|
249
|
+
WEB_SEARCH_CONTRACT,
|
|
250
|
+
WRITE_FILE_CONTRACT,
|
|
251
|
+
listCanonicalContracts,
|
|
252
|
+
registerCanonicalContracts
|
|
253
|
+
};
|
|
254
|
+
//# sourceMappingURL=contracts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/contracts.ts"],"sourcesContent":["/**\n * TITAN — Canonical Skill Contracts (v6.1.0-beta.15, Phase D.3)\n *\n * Zod-validated input contracts for the 10 most-used built-in skills.\n * Registered into the global contract registry at module load; the\n * tool runner looks them up by name and validates incoming args\n * BEFORE the skill's execute() runs.\n *\n * COVERAGE in this beta:\n *\n * - write_file (write, safe)\n * - read_file (read, safe)\n * - edit_file (write, safe)\n * - append_file (write, safe)\n * - list_dir (read, safe)\n * - web_search (network/read, moderate)\n * - web_fetch (network/read, moderate)\n * - download_image (network/write, moderate)\n * - shell (destructive, high)\n *\n * Not yet covered (~239 other tools): they continue to use the legacy\n * raw JSON-schema validation in toolRunner.ts. Migration is opt-in\n * per skill; this file adds new entries as more skills are upgraded.\n *\n * EACH CONTRACT also declares:\n * - sideEffects: drives Phase D.4 auto-mode classifier\n * - riskLevel: drives Phase D.4 approval gating\n * - exampleCalls: positive samples for tests + docs + classifier\n *\n * NAMING: contracts are named exactly as the skill name they\n * validate. The registry lookup in toolRunner.ts is keyed on name,\n * so a mismatch silently disables validation. The unit tests below\n * pin the exact names.\n */\n\nimport { z } from 'zod';\nimport { registerToolContract, type ToolContract } from '../../agent/toolContract.js';\n\n/* ────────────────────────── Filesystem ────────────────────────── */\n\nexport const WRITE_FILE_CONTRACT: ToolContract<\n { path: string; content: string },\n string\n> = {\n name: 'write_file',\n summary: 'Write content to a file. Overwrites if it exists. Path must be under home or /tmp.',\n input: z.object({\n path: z.string().min(1, 'path is required'),\n content: z.string(), // empty content is valid — touching a file\n }).passthrough(),\n sideEffects: ['write'],\n // beta.17 — writes are 'moderate' (was 'safe'). Codex P1 #2: writes\n // can overwrite files and mutate user state. The auto-mode classifier\n // maps 'moderate' to 'notify' under standard policy (runs but logs)\n // and 'gate' under 'paranoid'. Path-aware scoping (allowing /tmp/*\n // and ~/.titan/workspace/* writes to auto-run) is a follow-up — see\n // claude_notes in .ai_bridge.json.\n riskLevel: 'moderate',\n exampleCalls: [\n {\n description: 'write a small note',\n args: { path: '/tmp/note.txt', content: 'hello world\\n' },\n },\n {\n description: 'write an HTML report in the home dir',\n args: { path: '~/reports/q4.html', content: '<html>...</html>' },\n },\n ],\n};\n\nexport const READ_FILE_CONTRACT: ToolContract<\n { path: string; startLine?: number; endLine?: number },\n string\n> = {\n name: 'read_file',\n summary: 'Read a file from disk. Optional startLine/endLine for partial reads.',\n // beta.17 (Codex P1): matched to the actual filesystem.ts skill\n // shape — read_file accepts {path, startLine, endLine}. The\n // byteOffset/byteLimit fields the original contract had don't\n // exist on the skill; Zod was silently stripping them.\n // .passthrough() preserves any extras so a future skill addition\n // doesn't silently drop before we catch up the contract.\n input: z.object({\n path: z.string().min(1),\n startLine: z.number().int().nonnegative().optional(),\n endLine: z.number().int().nonnegative().optional(),\n }).passthrough(),\n sideEffects: ['read'],\n riskLevel: 'safe',\n exampleCalls: [\n {\n description: 'read a whole file',\n args: { path: '/tmp/note.txt' },\n },\n {\n description: 'read lines 100-200 of a file',\n args: { path: '/var/log/titan.log', startLine: 100, endLine: 200 },\n },\n ],\n};\n\nexport const EDIT_FILE_CONTRACT: ToolContract<\n { path: string; target: string; replacement: string },\n string\n> = {\n name: 'edit_file',\n summary: 'Find-and-replace exactly one occurrence of target with replacement.',\n input: z.object({\n path: z.string().min(1),\n target: z.string().min(1, 'target must not be empty'),\n replacement: z.string(),\n }).passthrough(),\n sideEffects: ['write'],\n // beta.17 — see WRITE_FILE_CONTRACT comment. Writes are 'moderate'.\n riskLevel: 'moderate',\n exampleCalls: [\n {\n description: 'fix a typo',\n args: { path: '/tmp/note.txt', target: 'teh', replacement: 'the' },\n },\n ],\n};\n\nexport const APPEND_FILE_CONTRACT: ToolContract<\n { path: string; content: string },\n string\n> = {\n name: 'append_file',\n summary: 'Append content to the end of a file. Creates the file if missing.',\n input: z.object({\n path: z.string().min(1),\n content: z.string().min(1, 'content must not be empty'),\n }).passthrough(),\n sideEffects: ['write'],\n // beta.17 — see WRITE_FILE_CONTRACT comment. Writes are 'moderate'.\n riskLevel: 'moderate',\n exampleCalls: [\n {\n description: 'append a log line',\n args: { path: '/tmp/build.log', content: 'INFO: build started\\n' },\n },\n ],\n};\n\nexport const LIST_DIR_CONTRACT: ToolContract<\n { path: string; recursive?: boolean },\n string\n> = {\n name: 'list_dir',\n summary: 'List the contents of a directory. Optionally recursive.',\n // beta.17 (Codex P1): matched to actual filesystem.ts skill shape.\n // The skill accepts {path, recursive}; the original contract added\n // a `pattern` field the skill doesn't read. Dropped.\n input: z.object({\n path: z.string().min(1),\n recursive: z.boolean().optional(),\n }).passthrough(),\n sideEffects: ['read'],\n riskLevel: 'safe',\n exampleCalls: [\n {\n description: 'list current directory contents',\n args: { path: '.' },\n },\n {\n description: 'list a tree recursively',\n args: { path: 'src', recursive: true },\n },\n ],\n};\n\n/* ────────────────────────── Network ────────────────────────── */\n\nexport const WEB_SEARCH_CONTRACT: ToolContract<\n { query: string; maxResults?: number },\n string\n> = {\n name: 'web_search',\n summary: 'Search the public web. Returns ranked results with titles, URLs, and snippets.',\n // beta.17 (Codex P1): the actual web_search.ts skill uses `maxResults`,\n // not `numResults`. The original contract silently dropped the\n // caller's count preference because Zod stripped the unknown\n // `maxResults` and the skill only saw `query`. Field renamed to\n // match; .passthrough() guards against future skill additions.\n input: z.object({\n query: z.string().min(1, 'query must not be empty'),\n maxResults: z.number().int().min(1).max(50).optional(),\n }).passthrough(),\n sideEffects: ['network', 'read'],\n riskLevel: 'moderate',\n exampleCalls: [\n {\n description: 'a simple lookup',\n args: { query: 'TypeScript discriminated union' },\n },\n {\n description: 'a focused lookup with explicit count',\n args: { query: 'discriminated unions', maxResults: 5 },\n },\n ],\n};\n\nexport const WEB_FETCH_CONTRACT: ToolContract<\n { url: string; extractMode?: 'markdown' | 'text'; maxChars?: number },\n string\n> = {\n name: 'web_fetch',\n summary: 'Fetch a URL over HTTP(S) and return its content as markdown or text. Internal/private network addresses are blocked.',\n // beta.17 (Codex P1): the actual web_fetch.ts skill accepts\n // {url, extractMode: 'markdown'|'text', maxChars}, NOT\n // {url, method, headers, body}. The original contract advertised a\n // generic HTTP-call shape the skill never had; LLMs passing\n // method/headers/body were getting them silently stripped.\n // Schema now mirrors the actual skill. .passthrough() preserves\n // any future additions.\n input: z.object({\n url: z.string().url('url must be a valid http(s) URL'),\n extractMode: z.enum(['markdown', 'text']).optional(),\n maxChars: z.number().int().positive().max(200_000).optional(),\n }).passthrough(),\n sideEffects: ['network', 'read'],\n riskLevel: 'moderate',\n exampleCalls: [\n {\n description: 'fetch an article as markdown',\n args: { url: 'https://example.com/article' },\n },\n {\n description: 'fetch as plain text with a tighter cap',\n args: { url: 'https://example.com/long-page', extractMode: 'text', maxChars: 8000 },\n },\n ],\n};\n\nexport const DOWNLOAD_IMAGE_CONTRACT: ToolContract<\n { url: string },\n string\n> = {\n name: 'download_image',\n summary: 'Download an image and return a tdi:// reference token for embedding in HTML.',\n // beta.17 (Codex P1 audit): download_image.ts skill takes {url} only;\n // contract matches. .passthrough() added defensively in case the\n // skill grows options later.\n input: z.object({\n url: z.string().url('url must be a valid http(s) URL'),\n }).passthrough(),\n sideEffects: ['network', 'write'],\n riskLevel: 'moderate',\n exampleCalls: [\n {\n description: 'download a public image',\n args: { url: 'https://upload.wikimedia.org/wikipedia/commons/example.jpg' },\n },\n ],\n};\n\n/* ────────────────────────── Shell (destructive) ────────────────────────── */\n\nexport const SHELL_CONTRACT: ToolContract<\n {\n command: string;\n cwd?: string;\n timeout?: number;\n background?: boolean;\n verify_port?: number;\n },\n string\n> = {\n name: 'shell',\n summary: 'Execute a shell command. Destructive — guarded by the sandbox + approval gate.',\n // beta.17 (Codex P1): matched to actual shell.ts skill shape. The\n // skill uses `timeout` (not `timeoutMs`) and also accepts\n // `background` + `verify_port` for dev-server launches. The\n // original contract used the wrong name + missed the background\n // path entirely. Zod was silently stripping `background` /\n // `verify_port` whenever the LLM tried to launch a background\n // process — the agent would then complain that the dev server\n // \"didn't start\" because shell took the command synchronously.\n input: z.object({\n command: z.string().min(1, 'command must not be empty'),\n cwd: z.string().optional(),\n timeout: z.number().int().positive().max(600_000).optional(),\n background: z.boolean().optional(),\n verify_port: z.number().int().min(1).max(65535).optional(),\n }).passthrough(),\n sideEffects: ['destructive'],\n riskLevel: 'high',\n exampleCalls: [\n {\n description: 'list files',\n args: { command: 'ls -la' },\n },\n {\n description: 'check disk usage in a specific dir',\n args: { command: 'du -sh', cwd: '/var/log' },\n },\n {\n description: 'launch a dev server in the background, wait for port 3000',\n args: { command: 'npm run dev', background: true, verify_port: 3000 },\n },\n ],\n};\n\n/* ────────────────────────── Registry init ────────────────────────── */\n\nconst CANONICAL_CONTRACTS: ToolContract[] = [\n WRITE_FILE_CONTRACT,\n READ_FILE_CONTRACT,\n EDIT_FILE_CONTRACT,\n APPEND_FILE_CONTRACT,\n LIST_DIR_CONTRACT,\n WEB_SEARCH_CONTRACT,\n WEB_FETCH_CONTRACT,\n DOWNLOAD_IMAGE_CONTRACT,\n SHELL_CONTRACT,\n];\n\n/**\n * Idempotent registration of all canonical contracts. Called from the\n * skill registry's `initBuiltinSkills` so the contracts are loaded\n * before any tool dispatch happens.\n */\nexport function registerCanonicalContracts(): void {\n for (const c of CANONICAL_CONTRACTS) {\n registerToolContract(c);\n }\n}\n\n/** For tests + introspection. */\nexport function listCanonicalContracts(): ToolContract[] {\n return [...CANONICAL_CONTRACTS];\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { registerSkill } from "../registry.js";
|
|
3
3
|
import { TITAN_VERSION } from "../../utils/constants.js";
|
|
4
4
|
import { registerImage } from "../../agent/imageRegistry.js";
|
|
5
|
+
import { verifyImageDownload, recordVerificationEvent } from "../../agent/verification.js";
|
|
5
6
|
function isInternalUrl(urlStr) {
|
|
6
7
|
try {
|
|
7
8
|
const u = new URL(urlStr);
|
|
@@ -134,6 +135,14 @@ NEVER use a fabricated URL. Only embed images whose URL came from an actual web_
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
const buf = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
138
|
+
const verifyResult = verifyImageDownload(buf, mimeType);
|
|
139
|
+
recordVerificationEvent({ kind: "image_download", result: verifyResult, artifactRef: url });
|
|
140
|
+
if (!verifyResult.passed) {
|
|
141
|
+
return JSON.stringify({
|
|
142
|
+
ok: false,
|
|
143
|
+
error: `${verifyResult.reason}. URL: ${url}. The CDN may have served a non-image response (HTML error page, redirect, hotlink-blocker). Try a different image source.`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
137
146
|
const base64 = buf.toString("base64");
|
|
138
147
|
const realDataUrl = `data:${mimeType};base64,${base64}`;
|
|
139
148
|
const ref = registerImage(realDataUrl, mimeType, buf.length);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/skills/builtin/download_image.ts"],"sourcesContent":["/**\n * TITAN — Download Image Skill (Built-in, v6.1.0-alpha.37)\n *\n * Fetches an image URL server-side and returns it as a base64\n * data URL the agent can embed directly in HTML as\n * `<img src=\"data:image/jpeg;base64,…\">`.\n *\n * Why this skill exists:\n *\n * The HTML report viewer (alpha.16 → alpha.36) went through five\n * rounds of iframe sandbox / CORS / referrer / CSP / Shadow-DOM\n * tuning trying to make external `<img src=\"https://…\">` tags load\n * reliably. They never did, consistently. Browsers, referer\n * policies, and image-host hotlinking rules kept stacking.\n *\n * Tony's call: \"instead of linking, the AI Agent downloads the\n * images?\" — exactly right. A base64 data URL is **portable**\n * (the HTML file is self-contained), **always loads** (no network\n * request, no CORS, no referer to police), and **survives sharing**\n * (downloading the HTML to your phone, emailing it, hosting it\n * elsewhere all keep the image intact).\n *\n * Trade: file size grows ~33% per image (base64 overhead). For an\n * essay with 3-5 images at typical web resolution, the file is\n * still well under 1 MB. We cap individual images at 4 MB of raw\n * bytes (≈5.3 MB base64) so a hostile or oversized image can't\n * blow out the HTML doc.\n *\n * Pattern mirrors web_fetch — same internal-URL protection, same\n * User-Agent, same timeout. Returns a string suitable for direct\n * use in an `<img src>` attribute.\n */\nimport { registerSkill } from '../registry.js';\nimport { TITAN_VERSION } from '../../utils/constants.js';\nimport { registerImage } from '../../agent/imageRegistry.js';\n\n// Block private-network / loopback addresses. Mirrors web_fetch.ts.\nfunction isInternalUrl(urlStr: string): boolean {\n try {\n const u = new URL(urlStr);\n const host = u.hostname.toLowerCase();\n if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return true;\n if (host.endsWith('.local') || host.endsWith('.internal')) return true;\n if (/^10\\./.test(host)) return true;\n if (/^192\\.168\\./.test(host)) return true;\n if (/^172\\.(1[6-9]|2\\d|3[0-1])\\./.test(host)) return true;\n if (/^169\\.254\\./.test(host)) return true; // link-local\n return false;\n } catch {\n return false;\n }\n}\n\nfunction inferMimeFromUrl(url: string, contentType: string | null): string {\n if (contentType && contentType.startsWith('image/')) {\n // strip charset / parameters if any\n return contentType.split(';')[0].trim();\n }\n const lower = url.toLowerCase();\n if (lower.endsWith('.png')) return 'image/png';\n if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';\n if (lower.endsWith('.gif')) return 'image/gif';\n if (lower.endsWith('.webp')) return 'image/webp';\n if (lower.endsWith('.svg')) return 'image/svg+xml';\n if (lower.endsWith('.bmp')) return 'image/bmp';\n if (lower.endsWith('.avif')) return 'image/avif';\n return 'image/jpeg'; // safe default; most photo CDNs serve JPEG\n}\n\nconst MAX_IMAGE_BYTES = 4 * 1024 * 1024; // 4 MB raw\n\nexport function registerDownloadImageSkill(): void {\n registerSkill(\n { name: 'download_image', description: 'Download an image as a base64 data URL for HTML embedding', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'download_image',\n description: `Download an image from a URL server-side and return a SHORT reference token (tdi://...) suitable for embedding directly in HTML as <img src=\"tdi://...\">.\n\nUSE WHEN: you are writing an HTML report and want to include images that will reliably display in the user's viewer, on any machine, even offline. The viewer's iframe sandbox + referer-policy + private-network mitigations can block hotlinked <img src=\"https://…\"> URLs from many hosts (Wikimedia, Pinterest, Imgur, etc.). Downloading and embedding sidesteps all of that.\n\nWORKFLOW:\n1. Find the image URL (typically via web_search results that include image links, or known Wikimedia Commons URLs from your research).\n2. Call download_image with the URL.\n3. Use the returned dataUrl string (a short \"tdi://abc123def456\" token, ~30 chars) directly as the src attribute of an <img> tag in your HTML.\n4. When write_file persists your HTML, the token is automatically substituted with the actual base64 data URL on disk. The final file is self-contained and portable.\n\nExample:\n Call: download_image({ url: \"https://upload.wikimedia.org/wikipedia/commons/0/05/Martin_Luther_King%2C_Jr..jpg\" })\n Returns: { ok: true, dataUrl: \"tdi://9f3a2c1b8e5d7f06\", sizeBytes: 191378, mimeType: \"image/jpeg\" }\n Then in your HTML: <img src=\"tdi://9f3a2c1b8e5d7f06\" alt=\"MLK portrait\" style=\"max-width:380px;\">\n\nIMPORTANT: the token is intentionally short. Do NOT try to \"expand\" it, \"decode\" it, or pass it through other tools — just paste it AS-IS inside <img src=\"...\">. The file-write tools handle the expansion automatically.\n\nDO NOT USE FOR:\n- Local files you already have on disk → use read_file or reference the path directly.\n- Generating images from a text prompt → use generate_image.\n- Inspecting an image's contents to describe it → use analyze_image.\n\nParameters:\n- url (string, required) — full HTTPS URL of the image. HTTP works too but HTTPS is strongly preferred. Internal/private addresses are blocked.\n\nReturns:\n- { ok: true, dataUrl, mimeType, sizeBytes } on success\n- { ok: false, error } on failure (HTTP error, too-large, internal URL, timeout, etc.)\n\nSize limits: individual image capped at 4 MB raw (≈5.3 MB base64). Larger images fail with a clear error — pick a smaller source or scale the URL via a CDN parameter.\n\nNEVER use a fabricated URL. Only embed images whose URL came from an actual web_search / web_fetch result. If you don't have a real source URL, OMIT the image rather than invent one.`,\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'Full http(s) URL of the image to download.' },\n },\n required: ['url'],\n },\n execute: async (args) => {\n const url = String(args.url ?? '').trim();\n if (!url) return JSON.stringify({ ok: false, error: 'url is required' });\n if (isInternalUrl(url)) {\n return JSON.stringify({ ok: false, error: 'Internal / private network URLs are not permitted.' });\n }\n let parsed: URL;\n try { parsed = new URL(url); }\n catch { return JSON.stringify({ ok: false, error: 'Invalid URL.' }); }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return JSON.stringify({ ok: false, error: 'Only http(s) URLs are supported.' });\n }\n try {\n const response = await fetch(url, {\n headers: {\n 'User-Agent': `Mozilla/5.0 (compatible; TITAN/${TITAN_VERSION}; +https://github.com/Djtony707/TITAN)`,\n // Identify as an image-fetcher so CDNs that vary by Accept-header serve raw bytes.\n 'Accept': 'image/*, */*;q=0.8',\n },\n signal: AbortSignal.timeout(20000),\n redirect: 'follow',\n });\n if (!response.ok) {\n return JSON.stringify({\n ok: false,\n error: `HTTP ${response.status} ${response.statusText} fetching ${url}`,\n });\n }\n const contentType = response.headers.get('content-type');\n const mimeType = inferMimeFromUrl(url, contentType);\n if (!mimeType.startsWith('image/')) {\n return JSON.stringify({\n ok: false,\n error: `Response is not an image (content-type=${contentType ?? 'unknown'}).`,\n });\n }\n // Stream-read with size cap so a 100MB \"image\" doesn't blow the process.\n const reader = response.body?.getReader();\n if (!reader) return JSON.stringify({ ok: false, error: 'No response body.' });\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n total += value.length;\n if (total > MAX_IMAGE_BYTES) {\n await reader.cancel().catch(() => { /* ignore */ });\n return JSON.stringify({\n ok: false,\n error: `Image exceeded ${MAX_IMAGE_BYTES / 1024 / 1024}MB cap (got at least ${(total / 1024 / 1024).toFixed(1)}MB). Pick a smaller image source or use a CDN URL with size parameters.`,\n });\n }\n }\n const buf = Buffer.concat(chunks.map(c => Buffer.from(c)));\n const base64 = buf.toString('base64');\n const realDataUrl = `data:${mimeType};base64,${base64}`;\n // v6.1.0-alpha.56 — register the real data URL in\n // the side-channel and return a short opaque token\n // (`tdi://abc123def456`) as the `dataUrl` field.\n // The LLM treats it like a normal URL and embeds\n // it as `<img src=\"tdi://...\">`. write_file resolves\n // the token to the full data URL right before\n // writing to disk, so the file on disk is portable\n // self-contained HTML with the image embedded.\n //\n // This fixes the long-standing \"Writer hotlinks\n // external URL instead of embedding\" problem,\n // whose root cause was never the tool itself — it\n // was that asking an LLM to copy 270 KB of base64\n // verbatim through its context window is fragile\n // even when nothing in the harness strips it.\n const ref = registerImage(realDataUrl, mimeType, buf.length);\n return JSON.stringify({\n ok: true,\n dataUrl: ref,\n mimeType,\n sizeBytes: buf.length,\n note: 'dataUrl is a short reference token; embed it directly as <img src=\"...\"> and the file-write tools will substitute the real image bytes before saving.',\n });\n } catch (err) {\n const e = err as Error;\n const msg = e.name === 'AbortError' ? 'Timed out after 20s' : e.message;\n return JSON.stringify({ ok: false, error: msg });\n }\n },\n },\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/skills/builtin/download_image.ts"],"sourcesContent":["/**\n * TITAN — Download Image Skill (Built-in, v6.1.0-alpha.37)\n *\n * Fetches an image URL server-side and returns it as a base64\n * data URL the agent can embed directly in HTML as\n * `<img src=\"data:image/jpeg;base64,…\">`.\n *\n * Why this skill exists:\n *\n * The HTML report viewer (alpha.16 → alpha.36) went through five\n * rounds of iframe sandbox / CORS / referrer / CSP / Shadow-DOM\n * tuning trying to make external `<img src=\"https://…\">` tags load\n * reliably. They never did, consistently. Browsers, referer\n * policies, and image-host hotlinking rules kept stacking.\n *\n * Tony's call: \"instead of linking, the AI Agent downloads the\n * images?\" — exactly right. A base64 data URL is **portable**\n * (the HTML file is self-contained), **always loads** (no network\n * request, no CORS, no referer to police), and **survives sharing**\n * (downloading the HTML to your phone, emailing it, hosting it\n * elsewhere all keep the image intact).\n *\n * Trade: file size grows ~33% per image (base64 overhead). For an\n * essay with 3-5 images at typical web resolution, the file is\n * still well under 1 MB. We cap individual images at 4 MB of raw\n * bytes (≈5.3 MB base64) so a hostile or oversized image can't\n * blow out the HTML doc.\n *\n * Pattern mirrors web_fetch — same internal-URL protection, same\n * User-Agent, same timeout. Returns a string suitable for direct\n * use in an `<img src>` attribute.\n */\nimport { registerSkill } from '../registry.js';\nimport { TITAN_VERSION } from '../../utils/constants.js';\nimport { registerImage } from '../../agent/imageRegistry.js';\nimport { verifyImageDownload, recordVerificationEvent } from '../../agent/verification.js';\n\n// Block private-network / loopback addresses. Mirrors web_fetch.ts.\nfunction isInternalUrl(urlStr: string): boolean {\n try {\n const u = new URL(urlStr);\n const host = u.hostname.toLowerCase();\n if (host === 'localhost' || host === '127.0.0.1' || host === '::1') return true;\n if (host.endsWith('.local') || host.endsWith('.internal')) return true;\n if (/^10\\./.test(host)) return true;\n if (/^192\\.168\\./.test(host)) return true;\n if (/^172\\.(1[6-9]|2\\d|3[0-1])\\./.test(host)) return true;\n if (/^169\\.254\\./.test(host)) return true; // link-local\n return false;\n } catch {\n return false;\n }\n}\n\nfunction inferMimeFromUrl(url: string, contentType: string | null): string {\n if (contentType && contentType.startsWith('image/')) {\n // strip charset / parameters if any\n return contentType.split(';')[0].trim();\n }\n const lower = url.toLowerCase();\n if (lower.endsWith('.png')) return 'image/png';\n if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';\n if (lower.endsWith('.gif')) return 'image/gif';\n if (lower.endsWith('.webp')) return 'image/webp';\n if (lower.endsWith('.svg')) return 'image/svg+xml';\n if (lower.endsWith('.bmp')) return 'image/bmp';\n if (lower.endsWith('.avif')) return 'image/avif';\n return 'image/jpeg'; // safe default; most photo CDNs serve JPEG\n}\n\nconst MAX_IMAGE_BYTES = 4 * 1024 * 1024; // 4 MB raw\n\nexport function registerDownloadImageSkill(): void {\n registerSkill(\n { name: 'download_image', description: 'Download an image as a base64 data URL for HTML embedding', version: '1.0.0', source: 'bundled', enabled: true },\n {\n name: 'download_image',\n description: `Download an image from a URL server-side and return a SHORT reference token (tdi://...) suitable for embedding directly in HTML as <img src=\"tdi://...\">.\n\nUSE WHEN: you are writing an HTML report and want to include images that will reliably display in the user's viewer, on any machine, even offline. The viewer's iframe sandbox + referer-policy + private-network mitigations can block hotlinked <img src=\"https://…\"> URLs from many hosts (Wikimedia, Pinterest, Imgur, etc.). Downloading and embedding sidesteps all of that.\n\nWORKFLOW:\n1. Find the image URL (typically via web_search results that include image links, or known Wikimedia Commons URLs from your research).\n2. Call download_image with the URL.\n3. Use the returned dataUrl string (a short \"tdi://abc123def456\" token, ~30 chars) directly as the src attribute of an <img> tag in your HTML.\n4. When write_file persists your HTML, the token is automatically substituted with the actual base64 data URL on disk. The final file is self-contained and portable.\n\nExample:\n Call: download_image({ url: \"https://upload.wikimedia.org/wikipedia/commons/0/05/Martin_Luther_King%2C_Jr..jpg\" })\n Returns: { ok: true, dataUrl: \"tdi://9f3a2c1b8e5d7f06\", sizeBytes: 191378, mimeType: \"image/jpeg\" }\n Then in your HTML: <img src=\"tdi://9f3a2c1b8e5d7f06\" alt=\"MLK portrait\" style=\"max-width:380px;\">\n\nIMPORTANT: the token is intentionally short. Do NOT try to \"expand\" it, \"decode\" it, or pass it through other tools — just paste it AS-IS inside <img src=\"...\">. The file-write tools handle the expansion automatically.\n\nDO NOT USE FOR:\n- Local files you already have on disk → use read_file or reference the path directly.\n- Generating images from a text prompt → use generate_image.\n- Inspecting an image's contents to describe it → use analyze_image.\n\nParameters:\n- url (string, required) — full HTTPS URL of the image. HTTP works too but HTTPS is strongly preferred. Internal/private addresses are blocked.\n\nReturns:\n- { ok: true, dataUrl, mimeType, sizeBytes } on success\n- { ok: false, error } on failure (HTTP error, too-large, internal URL, timeout, etc.)\n\nSize limits: individual image capped at 4 MB raw (≈5.3 MB base64). Larger images fail with a clear error — pick a smaller source or scale the URL via a CDN parameter.\n\nNEVER use a fabricated URL. Only embed images whose URL came from an actual web_search / web_fetch result. If you don't have a real source URL, OMIT the image rather than invent one.`,\n parameters: {\n type: 'object',\n properties: {\n url: { type: 'string', description: 'Full http(s) URL of the image to download.' },\n },\n required: ['url'],\n },\n execute: async (args) => {\n const url = String(args.url ?? '').trim();\n if (!url) return JSON.stringify({ ok: false, error: 'url is required' });\n if (isInternalUrl(url)) {\n return JSON.stringify({ ok: false, error: 'Internal / private network URLs are not permitted.' });\n }\n let parsed: URL;\n try { parsed = new URL(url); }\n catch { return JSON.stringify({ ok: false, error: 'Invalid URL.' }); }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return JSON.stringify({ ok: false, error: 'Only http(s) URLs are supported.' });\n }\n try {\n const response = await fetch(url, {\n headers: {\n 'User-Agent': `Mozilla/5.0 (compatible; TITAN/${TITAN_VERSION}; +https://github.com/Djtony707/TITAN)`,\n // Identify as an image-fetcher so CDNs that vary by Accept-header serve raw bytes.\n 'Accept': 'image/*, */*;q=0.8',\n },\n signal: AbortSignal.timeout(20000),\n redirect: 'follow',\n });\n if (!response.ok) {\n return JSON.stringify({\n ok: false,\n error: `HTTP ${response.status} ${response.statusText} fetching ${url}`,\n });\n }\n const contentType = response.headers.get('content-type');\n const mimeType = inferMimeFromUrl(url, contentType);\n if (!mimeType.startsWith('image/')) {\n return JSON.stringify({\n ok: false,\n error: `Response is not an image (content-type=${contentType ?? 'unknown'}).`,\n });\n }\n // Stream-read with size cap so a 100MB \"image\" doesn't blow the process.\n const reader = response.body?.getReader();\n if (!reader) return JSON.stringify({ ok: false, error: 'No response body.' });\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n total += value.length;\n if (total > MAX_IMAGE_BYTES) {\n await reader.cancel().catch(() => { /* ignore */ });\n return JSON.stringify({\n ok: false,\n error: `Image exceeded ${MAX_IMAGE_BYTES / 1024 / 1024}MB cap (got at least ${(total / 1024 / 1024).toFixed(1)}MB). Pick a smaller image source or use a CDN URL with size parameters.`,\n });\n }\n }\n const buf = Buffer.concat(chunks.map(c => Buffer.from(c)));\n // beta.9 — verify-before-embed. Sniff the magic header\n // against the claimed MIME type. Catches the \"CDN\n // served an HTML error page with content-type\n // image/jpeg\" failure mode that otherwise looks\n // like a successful download until the user opens\n // the report and sees broken images. On magic\n // mismatch we still register the bytes (the LLM\n // might be downloading SVG via a quirky CDN) but\n // we record the failure for the driver loop's\n // assertion counter.\n const verifyResult = verifyImageDownload(buf, mimeType);\n recordVerificationEvent({ kind: 'image_download', result: verifyResult, artifactRef: url });\n if (!verifyResult.passed) {\n return JSON.stringify({\n ok: false,\n error: `${verifyResult.reason}. URL: ${url}. The CDN may have served a non-image response (HTML error page, redirect, hotlink-blocker). Try a different image source.`,\n });\n }\n const base64 = buf.toString('base64');\n const realDataUrl = `data:${mimeType};base64,${base64}`;\n // v6.1.0-alpha.56 — register the real data URL in\n // the side-channel and return a short opaque token\n // (`tdi://abc123def456`) as the `dataUrl` field.\n // The LLM treats it like a normal URL and embeds\n // it as `<img src=\"tdi://...\">`. write_file resolves\n // the token to the full data URL right before\n // writing to disk, so the file on disk is portable\n // self-contained HTML with the image embedded.\n //\n // This fixes the long-standing \"Writer hotlinks\n // external URL instead of embedding\" problem,\n // whose root cause was never the tool itself — it\n // was that asking an LLM to copy 270 KB of base64\n // verbatim through its context window is fragile\n // even when nothing in the harness strips it.\n const ref = registerImage(realDataUrl, mimeType, buf.length);\n return JSON.stringify({\n ok: true,\n dataUrl: ref,\n mimeType,\n sizeBytes: buf.length,\n note: 'dataUrl is a short reference token; embed it directly as <img src=\"...\"> and the file-write tools will substitute the real image bytes before saving.',\n });\n } catch (err) {\n const e = err as Error;\n const msg = e.name === 'AbortError' ? 'Timed out after 20s' : e.message;\n return JSON.stringify({ ok: false, error: msg });\n }\n },\n },\n );\n}\n"],"mappings":"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","names":[]}
|