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,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, statSync, existsSync } from "fs";
|
|
3
|
+
function verifyFileWrite(path, expectedBytes, contentSample) {
|
|
4
|
+
const verifier = "fs.readback";
|
|
5
|
+
if (!existsSync(path)) {
|
|
6
|
+
return {
|
|
7
|
+
passed: false,
|
|
8
|
+
reason: `expected ${path} to exist after write \u2014 it does not`,
|
|
9
|
+
verifier,
|
|
10
|
+
confidence: 1
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
let actualBytes = 0;
|
|
14
|
+
try {
|
|
15
|
+
actualBytes = statSync(path).size;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return {
|
|
18
|
+
passed: false,
|
|
19
|
+
reason: `stat failed on ${path}: ${e.message}`,
|
|
20
|
+
verifier,
|
|
21
|
+
confidence: 1
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (actualBytes !== expectedBytes) {
|
|
25
|
+
return {
|
|
26
|
+
passed: false,
|
|
27
|
+
reason: `byte mismatch: wrote ${expectedBytes}, stat reports ${actualBytes}`,
|
|
28
|
+
verifier,
|
|
29
|
+
confidence: 1
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (contentSample && contentSample.length > 0) {
|
|
33
|
+
try {
|
|
34
|
+
const sampleSize = Math.min(contentSample.length, 256);
|
|
35
|
+
const fd = readFileSync(path, "utf-8").slice(0, sampleSize);
|
|
36
|
+
const expectedHead = contentSample.slice(0, sampleSize);
|
|
37
|
+
if (fd !== expectedHead) {
|
|
38
|
+
return {
|
|
39
|
+
passed: false,
|
|
40
|
+
reason: `head ${sampleSize}b drift after write`,
|
|
41
|
+
verifier,
|
|
42
|
+
confidence: 0.95
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return {
|
|
47
|
+
passed: false,
|
|
48
|
+
reason: `readback failed: ${e.message}`,
|
|
49
|
+
verifier,
|
|
50
|
+
confidence: 1
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
passed: true,
|
|
56
|
+
reason: `verified ${actualBytes}b on disk`,
|
|
57
|
+
verifier,
|
|
58
|
+
confidence: 1
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function verifyImageDownload(buf, expectedMimeType) {
|
|
62
|
+
const verifier = "image.magic";
|
|
63
|
+
if (!buf || buf.length < 4) {
|
|
64
|
+
return {
|
|
65
|
+
passed: false,
|
|
66
|
+
reason: `image buffer too small (${buf?.length ?? 0} bytes)`,
|
|
67
|
+
verifier,
|
|
68
|
+
confidence: 1
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const checks = {
|
|
72
|
+
"image/png": (b) => b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71,
|
|
73
|
+
"image/jpeg": (b) => b[0] === 255 && b[1] === 216 && b[2] === 255,
|
|
74
|
+
"image/gif": (b) => b.slice(0, 6).toString("ascii") === "GIF87a" || b.slice(0, 6).toString("ascii") === "GIF89a",
|
|
75
|
+
"image/webp": (b) => b.slice(0, 4).toString("ascii") === "RIFF" && b.slice(8, 12).toString("ascii") === "WEBP",
|
|
76
|
+
"image/bmp": (b) => b[0] === 66 && b[1] === 77,
|
|
77
|
+
// AVIF: 'ftypavif' or 'ftypavis' at offset 4
|
|
78
|
+
"image/avif": (b) => b.length >= 12 && b.slice(4, 8).toString("ascii") === "ftyp" && (b.slice(8, 12).toString("ascii") === "avif" || b.slice(8, 12).toString("ascii") === "avis")
|
|
79
|
+
};
|
|
80
|
+
const check = checks[expectedMimeType];
|
|
81
|
+
if (!check) {
|
|
82
|
+
if (expectedMimeType === "image/svg+xml") {
|
|
83
|
+
const head = buf.slice(0, 100).toString("utf-8").trim().toLowerCase();
|
|
84
|
+
const looksLikeSvg = head.startsWith("<svg") || head.startsWith("<?xml") || head.includes("<svg");
|
|
85
|
+
return {
|
|
86
|
+
passed: looksLikeSvg,
|
|
87
|
+
reason: looksLikeSvg ? "svg-like header present" : "no <svg root tag in first 100 bytes",
|
|
88
|
+
verifier,
|
|
89
|
+
confidence: 0.7
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
passed: true,
|
|
94
|
+
reason: `no magic check for ${expectedMimeType} \u2014 accepting on size`,
|
|
95
|
+
verifier,
|
|
96
|
+
confidence: 0.5
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const matched = check(buf);
|
|
100
|
+
return {
|
|
101
|
+
passed: matched,
|
|
102
|
+
reason: matched ? `magic header matches ${expectedMimeType}` : `magic header MISMATCH for ${expectedMimeType} \u2014 got 0x${buf.slice(0, 4).toString("hex")}`,
|
|
103
|
+
verifier,
|
|
104
|
+
confidence: 1
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const RING_CAPACITY = 256;
|
|
108
|
+
const ring = [];
|
|
109
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
110
|
+
function subscribeVerification(fn) {
|
|
111
|
+
listeners.add(fn);
|
|
112
|
+
return () => {
|
|
113
|
+
listeners.delete(fn);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function recordVerificationEvent(event) {
|
|
117
|
+
const at = event.at ?? Date.now();
|
|
118
|
+
const full = { at, kind: event.kind, result: event.result, artifactRef: event.artifactRef };
|
|
119
|
+
ring.push(full);
|
|
120
|
+
if (ring.length > RING_CAPACITY) ring.splice(0, ring.length - RING_CAPACITY);
|
|
121
|
+
for (const listener of listeners) {
|
|
122
|
+
try {
|
|
123
|
+
listener(full);
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function recentVerificationEvents(limit = 32) {
|
|
129
|
+
return ring.slice(-limit);
|
|
130
|
+
}
|
|
131
|
+
function consecutiveTrailingFailures(forKinds) {
|
|
132
|
+
let n = 0;
|
|
133
|
+
for (let i = ring.length - 1; i >= 0; i--) {
|
|
134
|
+
const e = ring[i];
|
|
135
|
+
if (forKinds && !forKinds.includes(e.kind)) continue;
|
|
136
|
+
if (e.result.passed) return n;
|
|
137
|
+
n++;
|
|
138
|
+
}
|
|
139
|
+
return n;
|
|
140
|
+
}
|
|
141
|
+
function clearVerificationRing() {
|
|
142
|
+
ring.length = 0;
|
|
143
|
+
}
|
|
144
|
+
function verificationEventsSince(sinceMs, forKinds) {
|
|
145
|
+
return ring.filter(
|
|
146
|
+
(e) => e.at >= sinceMs && (forKinds ? forKinds.includes(e.kind) : true)
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function formatVerificationSuffix(v) {
|
|
150
|
+
if (v.passed) return ` [verify: ${v.reason}]`;
|
|
151
|
+
return ` [\u26A0 verify FAIL: ${v.reason}]`;
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
clearVerificationRing,
|
|
155
|
+
consecutiveTrailingFailures,
|
|
156
|
+
formatVerificationSuffix,
|
|
157
|
+
recentVerificationEvents,
|
|
158
|
+
recordVerificationEvent,
|
|
159
|
+
subscribeVerification,
|
|
160
|
+
verificationEventsSince,
|
|
161
|
+
verifyFileWrite,
|
|
162
|
+
verifyImageDownload
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=verification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/verification.ts"],"sourcesContent":["/**\n * TITAN — Runtime verification primitives (v6.1.0-beta.9)\n *\n * \"Verify-before-claim\" runtime helpers used by skills that produce\n * artifacts (write_file, append_file, edit_file, download_image, …).\n * The idea: every tool call that asserts \"I did X\" must produce a\n * checkable artifact, and the agent must read that artifact back\n * before reporting completion.\n *\n * The motivating problem: benchmark-cheating in the wider agent\n * ecosystem (UC Berkeley CRDI showed that all eight major agent\n * benchmarks are exploitable via reward-hacking; one model copied\n * answers from `git log`). The defense is not to chase a benchmark\n * number — it's to make every claim independently checkable AT\n * RUNTIME, regardless of which model is driving.\n *\n * This module gives the rest of the codebase three primitives:\n *\n * - `verifyFileWrite(path, expectedBytesWritten, contentSample?)`\n * Reads the file back. Confirms it exists, the byte count\n * matches the intended write, and (if a `contentSample` was\n * given) that the first ~256 bytes round-trip correctly.\n * Surfaces any drift as a `VerificationResult` the caller can\n * attach to its return value.\n *\n * - `verifyImageDownload(buf, expectedMimeType)`\n * Sniffs the first ~12 bytes for a magic-number match\n * against the claimed MIME type. PNG, JPEG, GIF, WebP, BMP,\n * AVIF are recognized. SVG and unknown types pass through\n * with `confidence < 1`. Catches the \"CDN served HTML\n * instead of an image\" failure mode that previously slipped\n * through silently.\n *\n * - `recordVerificationEvent(event)` / `recentVerificationEvents()`\n * Pushes to a bounded in-memory ring buffer that the driver\n * loop reads to compute `consecutiveAssertionFailures`, and\n * that the UI can subscribe to for trust-line gating.\n *\n * Failure semantics: a failed verification does NOT throw or abort\n * the write — the file is already on disk by the time we're called.\n * Instead it RETURNS a structured `VerificationResult` so the caller\n * can decide whether to surface it as a non-fatal warning or as\n * an error to the driver loop. The driver loop's\n * `consecutiveAssertionFailures` counter (added to DriverSubtaskState\n * in this same release) decides when accumulated failures escalate\n * into a subtask-fail.\n *\n * Why a separate module: tests need to import this WITHOUT pulling\n * the whole skill registry. The verifier helpers are pure functions\n * + a tiny in-memory store, so they're cheap to load in isolation.\n */\n\nimport { readFileSync, statSync, existsSync } from 'fs';\n\n/* ─────────────────────────── Types ─────────────────────────── */\n\nexport type VerificationKind =\n | 'file_write'\n | 'file_append'\n | 'file_edit'\n | 'image_download'\n | 'browser_fetch';\n\nexport interface VerificationResult {\n /** Did the artifact match expectations? */\n passed: boolean;\n /** Short human-readable diagnostic. Goes into the tool return string. */\n reason: string;\n /** Stable id of the verifier that ran (for telemetry + tests). */\n verifier: string;\n /** 0–1. 1 = bit-exact match; lower = heuristic match. */\n confidence: number;\n}\n\nexport interface VerificationEvent {\n /** Wall-clock ms at the time the event was recorded. */\n at: number;\n /** Which artifact kind was verified. */\n kind: VerificationKind;\n /** Same result fields the caller attached to its return value. */\n result: VerificationResult;\n /** Best-effort identifier for the artifact (path / URL / hash). */\n artifactRef: string;\n}\n\n/* ─────────────────────────── File write verification ─────────────────────────── */\n\n/**\n * Verify a file write by reading the artifact back.\n *\n * We re-stat the path and confirm:\n * 1. The file exists.\n * 2. Its size matches `expectedBytes` exactly (utf-8 byte count).\n * 3. (Optional) the first `contentSample.length` bytes match\n * `contentSample`. Sampling rather than full-file readback so\n * we don't double the IO cost for large writes.\n *\n * Returns `{ passed: false }` if any check fails. Returns\n * `{ passed: true, confidence: 1 }` only if all checks succeed.\n *\n * The 200-byte sample size is a deliberate trade — large enough to\n * catch encoding drift (BOM, line-ending munging, base64 garbling)\n * but small enough that a 50 MB HTML report doesn't cost two reads.\n */\nexport function verifyFileWrite(\n path: string,\n expectedBytes: number,\n contentSample?: string,\n): VerificationResult {\n const verifier = 'fs.readback';\n if (!existsSync(path)) {\n return {\n passed: false,\n reason: `expected ${path} to exist after write — it does not`,\n verifier,\n confidence: 1,\n };\n }\n let actualBytes = 0;\n try {\n actualBytes = statSync(path).size;\n } catch (e) {\n return {\n passed: false,\n reason: `stat failed on ${path}: ${(e as Error).message}`,\n verifier,\n confidence: 1,\n };\n }\n if (actualBytes !== expectedBytes) {\n return {\n passed: false,\n reason: `byte mismatch: wrote ${expectedBytes}, stat reports ${actualBytes}`,\n verifier,\n confidence: 1,\n };\n }\n if (contentSample && contentSample.length > 0) {\n try {\n const sampleSize = Math.min(contentSample.length, 256);\n const fd = readFileSync(path, 'utf-8').slice(0, sampleSize);\n const expectedHead = contentSample.slice(0, sampleSize);\n if (fd !== expectedHead) {\n return {\n passed: false,\n reason: `head ${sampleSize}b drift after write`,\n verifier,\n confidence: 0.95,\n };\n }\n } catch (e) {\n return {\n passed: false,\n reason: `readback failed: ${(e as Error).message}`,\n verifier,\n confidence: 1,\n };\n }\n }\n return {\n passed: true,\n reason: `verified ${actualBytes}b on disk`,\n verifier,\n confidence: 1,\n };\n}\n\n/* ─────────────────────────── Image download verification ─────────────────────────── */\n\n/**\n * Sniff the first ~12 bytes of a buffer for a magic-number match\n * against the claimed MIME type. Catches the \"the CDN served an\n * HTML error page with content-type image/jpeg\" failure mode that\n * otherwise looks like a successful download until the user opens\n * the HTML report and sees broken images.\n *\n * SVG and unknown formats can't be magic-checked from 12 bytes\n * (SVG is text, AVIF varies, etc.) so they pass with\n * `confidence: 0.5` — a callable check ran, but it can't be\n * bit-exact.\n *\n * Magic-number table sourced from the file(1) signature DB and\n * the W3C Media Type registry, restricted to the formats\n * `inferMimeFromUrl()` in download_image.ts emits.\n */\nexport function verifyImageDownload(\n buf: Buffer,\n expectedMimeType: string,\n): VerificationResult {\n const verifier = 'image.magic';\n if (!buf || buf.length < 4) {\n return {\n passed: false,\n reason: `image buffer too small (${buf?.length ?? 0} bytes)`,\n verifier,\n confidence: 1,\n };\n }\n // Magic-number checks. Each entry returns true if the buffer\n // starts with the relevant signature for `mime`.\n const checks: Record<string, (b: Buffer) => boolean> = {\n 'image/png': b => b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47,\n 'image/jpeg': b => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff,\n 'image/gif': b => b.slice(0, 6).toString('ascii') === 'GIF87a' || b.slice(0, 6).toString('ascii') === 'GIF89a',\n 'image/webp': b => b.slice(0, 4).toString('ascii') === 'RIFF' && b.slice(8, 12).toString('ascii') === 'WEBP',\n 'image/bmp': b => b[0] === 0x42 && b[1] === 0x4d,\n // AVIF: 'ftypavif' or 'ftypavis' at offset 4\n 'image/avif': b => b.length >= 12 && b.slice(4, 8).toString('ascii') === 'ftyp' && (b.slice(8, 12).toString('ascii') === 'avif' || b.slice(8, 12).toString('ascii') === 'avis'),\n };\n const check = checks[expectedMimeType];\n if (!check) {\n // SVG (XML), unknown types — best-effort acceptance.\n if (expectedMimeType === 'image/svg+xml') {\n const head = buf.slice(0, 100).toString('utf-8').trim().toLowerCase();\n const looksLikeSvg = head.startsWith('<svg') || head.startsWith('<?xml') || head.includes('<svg');\n return {\n passed: looksLikeSvg,\n reason: looksLikeSvg ? 'svg-like header present' : 'no <svg root tag in first 100 bytes',\n verifier,\n confidence: 0.7,\n };\n }\n return {\n passed: true,\n reason: `no magic check for ${expectedMimeType} — accepting on size`,\n verifier,\n confidence: 0.5,\n };\n }\n const matched = check(buf);\n return {\n passed: matched,\n reason: matched\n ? `magic header matches ${expectedMimeType}`\n : `magic header MISMATCH for ${expectedMimeType} — got 0x${buf.slice(0, 4).toString('hex')}`,\n verifier,\n confidence: 1,\n };\n}\n\n/* ─────────────────────────── Event ring buffer ─────────────────────────── */\n\n/**\n * Bounded ring buffer of recent verification events. Read by the\n * driver loop's `consecutiveAssertionFailures` calculator and by\n * any UI surface that wants to render a trust-line.\n *\n * Capped at 256 entries — plenty for a long mission's worth of\n * writes, small enough that we never hold the buffer for memory.\n */\nconst RING_CAPACITY = 256;\nconst ring: VerificationEvent[] = [];\n\n/** Live subscribers — see subscribeVerification. */\ntype VerificationListener = (event: VerificationEvent) => void;\nconst listeners = new Set<VerificationListener>();\n\n/**\n * Subscribe to live verification events. Returns an unsubscribe\n * function. Used by the gateway SSE endpoint and by any in-process\n * listener that wants real-time trust signal.\n */\nexport function subscribeVerification(fn: VerificationListener): () => void {\n listeners.add(fn);\n return () => { listeners.delete(fn); };\n}\n\nexport function recordVerificationEvent(event: Omit<VerificationEvent, 'at'> & { at?: number }): void {\n const at = event.at ?? Date.now();\n const full: VerificationEvent = { at, kind: event.kind, result: event.result, artifactRef: event.artifactRef };\n ring.push(full);\n if (ring.length > RING_CAPACITY) ring.splice(0, ring.length - RING_CAPACITY);\n // beta.10 — fan-out to live subscribers (SSE stream, in-process\n // listeners). Errors in any one subscriber are swallowed so a\n // misbehaving listener can't break the recorder for the rest.\n for (const listener of listeners) {\n try { listener(full); } catch { /* never let a bad subscriber affect the recorder */ }\n }\n}\n\nexport function recentVerificationEvents(limit = 32): VerificationEvent[] {\n return ring.slice(-limit);\n}\n\n/**\n * Count consecutive trailing verification failures. Used by the\n * driver loop to decide whether to escalate. Walks the ring from\n * newest to oldest, stopping at the first `passed: true`.\n *\n * `forKinds` filters which artifact kinds count (defaults to all).\n * Pass e.g. `['file_write','file_edit']` if a subtask only writes\n * files and you don't want unrelated browser fetches to influence\n * its failure count.\n */\nexport function consecutiveTrailingFailures(forKinds?: VerificationKind[]): number {\n let n = 0;\n for (let i = ring.length - 1; i >= 0; i--) {\n const e = ring[i];\n if (forKinds && !forKinds.includes(e.kind)) continue;\n if (e.result.passed) return n;\n n++;\n }\n return n;\n}\n\n/** For tests. Safe to call in prod too. */\nexport function clearVerificationRing(): void {\n ring.length = 0;\n}\n\n/**\n * Return every event whose `at >= sinceMs`, optionally filtered by\n * artifact kind. The driver loop uses this to attribute verification\n * events to the currently-running spawn (events recorded between\n * spawn-start and spawn-return belong to that spawn).\n *\n * Returns a fresh array; safe to iterate without locking. Order is\n * preserved (oldest → newest).\n */\nexport function verificationEventsSince(\n sinceMs: number,\n forKinds?: VerificationKind[],\n): VerificationEvent[] {\n return ring.filter(e =>\n e.at >= sinceMs && (forKinds ? forKinds.includes(e.kind) : true),\n );\n}\n\n\n/**\n * Format a `VerificationResult` for inclusion in a tool's return\n * string. Returns a leading space + bracketed status so concatenation\n * stays clean: `Successfully wrote 1234b to /path [verified 1234b on disk]`.\n *\n * Failed verifications get a \"⚠\" prefix so they're visually distinct\n * when the agent reads them back, without breaking the existing\n * \"Successfully …\" return format that downstream parsers may rely on.\n */\nexport function formatVerificationSuffix(v: VerificationResult): string {\n if (v.passed) return ` [verify: ${v.reason}]`;\n return ` [⚠ verify FAIL: ${v.reason}]`;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
|
3
|
+
import { dirname, join, resolve } from "path";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { TITAN_HOME } from "../utils/constants.js";
|
|
6
|
+
import logger from "../utils/logger.js";
|
|
7
|
+
const COMPONENT = "WorktreeExecutor";
|
|
8
|
+
const WORKTREES_DIR = join(TITAN_HOME, "worktrees");
|
|
9
|
+
function sanitizeSpawnId(spawnId) {
|
|
10
|
+
const clean = spawnId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 80);
|
|
11
|
+
return clean || `spawn-${Date.now()}`;
|
|
12
|
+
}
|
|
13
|
+
const orphanedRunsBySpawn = /* @__PURE__ */ new Map();
|
|
14
|
+
const lastWorktreePathBySpawn = /* @__PURE__ */ new Map();
|
|
15
|
+
function worktreePathFor(spawnId) {
|
|
16
|
+
return join(WORKTREES_DIR, sanitizeSpawnId(spawnId));
|
|
17
|
+
}
|
|
18
|
+
function temporaryWorktreePathFor(spawnId) {
|
|
19
|
+
return `${worktreePathFor(spawnId)}-${Date.now()}-${process.pid}`;
|
|
20
|
+
}
|
|
21
|
+
function runGit(args, cwd) {
|
|
22
|
+
return execFileSync("git", args, {
|
|
23
|
+
cwd,
|
|
24
|
+
encoding: "utf-8",
|
|
25
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
+
timeout: 6e4
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function runGitWithInput(args, cwd, input) {
|
|
30
|
+
return execFileSync("git", args, {
|
|
31
|
+
cwd,
|
|
32
|
+
input,
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
35
|
+
timeout: 6e4
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function applyParentDiff(worktreePath, baseCwd) {
|
|
39
|
+
const diff = runGit(["diff", "--binary", "HEAD", "--"], baseCwd);
|
|
40
|
+
if (diff.length === 0) return;
|
|
41
|
+
runGitWithInput(["apply", "--binary", "--whitespace=nowarn"], worktreePath, diff);
|
|
42
|
+
}
|
|
43
|
+
function copyParentUntrackedFiles(worktreePath, baseCwd) {
|
|
44
|
+
const raw = runGit(["ls-files", "--others", "--exclude-standard", "-z"], baseCwd);
|
|
45
|
+
for (const relativePath of raw.split("\0").filter(Boolean)) {
|
|
46
|
+
const source = resolve(baseCwd, relativePath);
|
|
47
|
+
const target = resolve(worktreePath, relativePath);
|
|
48
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
49
|
+
cpSync(source, target, { recursive: true, force: true, dereference: false });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function syncParentState(worktreePath, baseCwd) {
|
|
53
|
+
try {
|
|
54
|
+
applyParentDiff(worktreePath, baseCwd);
|
|
55
|
+
copyParentUntrackedFiles(worktreePath, baseCwd);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
cleanupWorktreePath(worktreePath, baseCwd);
|
|
58
|
+
throw new Error(`Could not mirror parent workspace state into ${worktreePath}: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function ensureWorktree(spawnId, baseCwd) {
|
|
62
|
+
mkdirSync(WORKTREES_DIR, { recursive: true });
|
|
63
|
+
const hasOrphanedRun = (orphanedRunsBySpawn.get(spawnId) ?? 0) > 0;
|
|
64
|
+
const worktreePath = hasOrphanedRun ? temporaryWorktreePathFor(spawnId) : worktreePathFor(spawnId);
|
|
65
|
+
if (existsSync(worktreePath)) {
|
|
66
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
runGit(["worktree", "prune"], baseCwd);
|
|
70
|
+
runGit(["worktree", "add", "--detach", worktreePath, "HEAD"], baseCwd);
|
|
71
|
+
syncParentState(worktreePath, baseCwd);
|
|
72
|
+
return worktreePath;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
logger.warn(COMPONENT, `git worktree add failed for ${worktreePath}; falling back to local clone: ${err.message}`);
|
|
75
|
+
try {
|
|
76
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
execFileSync("git", ["clone", "--local", "--no-hardlinks", baseCwd, worktreePath], {
|
|
81
|
+
encoding: "utf-8",
|
|
82
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
83
|
+
timeout: 6e4
|
|
84
|
+
});
|
|
85
|
+
runGit(["checkout", "--detach", "HEAD"], worktreePath);
|
|
86
|
+
syncParentState(worktreePath, baseCwd);
|
|
87
|
+
return worktreePath;
|
|
88
|
+
} catch (cloneErr) {
|
|
89
|
+
try {
|
|
90
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Could not create isolated worktree ${worktreePath}: ${cloneErr.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function cleanupWorktreePath(worktreePath, baseCwd) {
|
|
98
|
+
try {
|
|
99
|
+
runGit(["worktree", "remove", "--force", worktreePath], baseCwd);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
logger.debug(COMPONENT, `git worktree remove skipped for ${worktreePath}: ${err.message}`);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
105
|
+
} catch (err) {
|
|
106
|
+
logger.warn(COMPONENT, `Failed to remove worktree directory ${worktreePath}: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function markOrphanedRun(spawnId) {
|
|
110
|
+
orphanedRunsBySpawn.set(spawnId, (orphanedRunsBySpawn.get(spawnId) ?? 0) + 1);
|
|
111
|
+
let released = false;
|
|
112
|
+
return () => {
|
|
113
|
+
if (released) return;
|
|
114
|
+
released = true;
|
|
115
|
+
const next = (orphanedRunsBySpawn.get(spawnId) ?? 1) - 1;
|
|
116
|
+
if (next <= 0) {
|
|
117
|
+
orphanedRunsBySpawn.delete(spawnId);
|
|
118
|
+
} else {
|
|
119
|
+
orphanedRunsBySpawn.set(spawnId, next);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function listUntrackedFiles(worktreePath) {
|
|
124
|
+
try {
|
|
125
|
+
const raw = runGit(["ls-files", "--others", "--exclude-standard", "-z"], worktreePath);
|
|
126
|
+
return raw.split("\0").filter(Boolean);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
logger.debug(COMPONENT, `Could not list untracked files in ${worktreePath}: ${err.message}`);
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function diffUntrackedFile(worktreePath, relativePath) {
|
|
133
|
+
const absolutePath = resolve(worktreePath, relativePath);
|
|
134
|
+
try {
|
|
135
|
+
return execFileSync("git", ["diff", "--no-color", "--no-index", "--", "/dev/null", absolutePath], {
|
|
136
|
+
cwd: worktreePath,
|
|
137
|
+
encoding: "utf-8",
|
|
138
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
139
|
+
timeout: 6e4
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
const output = err.stdout;
|
|
143
|
+
return typeof output === "string" ? output : "";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function run(options) {
|
|
147
|
+
const baseCwd = options.baseCwd ?? process.cwd();
|
|
148
|
+
const signal = options.signal;
|
|
149
|
+
const worktreePath = ensureWorktree(options.spawnId, baseCwd);
|
|
150
|
+
const isolatedArgs = { ...options.args, cwd: worktreePath };
|
|
151
|
+
let executeSettled = false;
|
|
152
|
+
let releaseOrphanedRun;
|
|
153
|
+
let removeAbortListener;
|
|
154
|
+
try {
|
|
155
|
+
const executePromise = options.execute(isolatedArgs, { signal }).finally(() => {
|
|
156
|
+
executeSettled = true;
|
|
157
|
+
releaseOrphanedRun?.();
|
|
158
|
+
});
|
|
159
|
+
const abortPromise = signal ? new Promise((_, reject) => {
|
|
160
|
+
const onAbort = () => {
|
|
161
|
+
if (!executeSettled) {
|
|
162
|
+
releaseOrphanedRun = markOrphanedRun(options.spawnId);
|
|
163
|
+
}
|
|
164
|
+
cleanupWorktreePath(worktreePath, baseCwd);
|
|
165
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("Isolated worktree execution aborted"));
|
|
166
|
+
};
|
|
167
|
+
if (signal.aborted) {
|
|
168
|
+
onAbort();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
172
|
+
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
173
|
+
}) : void 0;
|
|
174
|
+
const result = await (abortPromise ? Promise.race([executePromise, abortPromise]) : executePromise);
|
|
175
|
+
lastWorktreePathBySpawn.set(options.spawnId, worktreePath);
|
|
176
|
+
return result;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
cleanupWorktreePath(worktreePath, baseCwd);
|
|
179
|
+
throw err;
|
|
180
|
+
} finally {
|
|
181
|
+
removeAbortListener?.();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function getWorktreeDiff(spawnId) {
|
|
185
|
+
const rememberedPath = lastWorktreePathBySpawn.get(spawnId);
|
|
186
|
+
const worktreePath = rememberedPath && existsSync(rememberedPath) ? rememberedPath : worktreePathFor(spawnId);
|
|
187
|
+
if (!existsSync(worktreePath)) return "";
|
|
188
|
+
let diff = "";
|
|
189
|
+
try {
|
|
190
|
+
diff += runGit(["diff", "--no-color", "HEAD", "--"], worktreePath);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
logger.warn(COMPONENT, `Could not diff tracked files in ${worktreePath}: ${err.message}`);
|
|
193
|
+
}
|
|
194
|
+
for (const relativePath of listUntrackedFiles(worktreePath)) {
|
|
195
|
+
diff += diffUntrackedFile(worktreePath, relativePath);
|
|
196
|
+
}
|
|
197
|
+
return diff;
|
|
198
|
+
}
|
|
199
|
+
function _worktreeRootForTests() {
|
|
200
|
+
return WORKTREES_DIR;
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
_worktreeRootForTests,
|
|
204
|
+
getWorktreeDiff,
|
|
205
|
+
run
|
|
206
|
+
};
|
|
207
|
+
//# sourceMappingURL=worktreeExecutor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/agent/worktreeExecutor.ts"],"sourcesContent":["/**\n * TITAN — Worktree Executor (Phase D.5)\n *\n * WHAT: Runs destructive contract-backed tool calls inside a detached\n * git worktree under TITAN_HOME/worktrees/<spawnId>/ and exposes the\n * resulting diff for review.\n *\n * WHY: Shell-like skills need real filesystem access, but their\n * blast radius should not include the live checkout by default. A\n * per-call worktree gives destructive tools a repo-shaped sandbox\n * while preserving ordinary tool dispatch when the feature is off.\n *\n * Parent checkout state is mirrored at worktree creation time with\n * `git diff HEAD` plus non-ignored untracked files. WHY: destructive\n * verify calls must test the user's current edits, not stale HEAD.\n * TRADE-OFFS: this is a per-call snapshot, so later parent edits need\n * a new isolated run; spawn-scoped reuse would need lifecycle hooks\n * the runner does not own yet.\n *\n * TRADE-OFFS: This isolates repository writes, not arbitrary absolute\n * paths a command might touch. The runner overrides cwd, but tools\n * that deliberately ignore cwd can still act outside the worktree;\n * guardrails and approval gates remain part of the defense. If the\n * host forbids writes to .git/worktrees, we fall back to a local clone\n * so isolation still works in locked-down test runners.\n */\nimport { cpSync, existsSync, mkdirSync, rmSync } from 'fs';\nimport { dirname, join, resolve } from 'path';\nimport { execFileSync } from 'child_process';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'WorktreeExecutor';\nconst WORKTREES_DIR = join(TITAN_HOME, 'worktrees');\n\nexport interface WorktreeRunOptions {\n spawnId: string;\n args: Record<string, unknown>;\n execute: (args: Record<string, unknown>, context?: { signal?: AbortSignal }) => Promise<string>;\n baseCwd?: string;\n signal?: AbortSignal;\n}\n\nfunction sanitizeSpawnId(spawnId: string): string {\n const clean = spawnId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 80);\n return clean || `spawn-${Date.now()}`;\n}\n\nconst orphanedRunsBySpawn = new Map<string, number>();\nconst lastWorktreePathBySpawn = new Map<string, string>();\n\nfunction worktreePathFor(spawnId: string): string {\n return join(WORKTREES_DIR, sanitizeSpawnId(spawnId));\n}\n\nfunction temporaryWorktreePathFor(spawnId: string): string {\n return `${worktreePathFor(spawnId)}-${Date.now()}-${process.pid}`;\n}\n\nfunction runGit(args: string[], cwd: string): string {\n return execFileSync('git', args, {\n cwd,\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: 60_000,\n });\n}\n\nfunction runGitWithInput(args: string[], cwd: string, input: string): string {\n return execFileSync('git', args, {\n cwd,\n input,\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'pipe'],\n timeout: 60_000,\n });\n}\n\nfunction applyParentDiff(worktreePath: string, baseCwd: string): void {\n const diff = runGit(['diff', '--binary', 'HEAD', '--'], baseCwd);\n if (diff.length === 0) return;\n runGitWithInput(['apply', '--binary', '--whitespace=nowarn'], worktreePath, diff);\n}\n\nfunction copyParentUntrackedFiles(worktreePath: string, baseCwd: string): void {\n const raw = runGit(['ls-files', '--others', '--exclude-standard', '-z'], baseCwd);\n for (const relativePath of raw.split('\\0').filter(Boolean)) {\n const source = resolve(baseCwd, relativePath);\n const target = resolve(worktreePath, relativePath);\n mkdirSync(dirname(target), { recursive: true });\n cpSync(source, target, { recursive: true, force: true, dereference: false });\n }\n}\n\nfunction syncParentState(worktreePath: string, baseCwd: string): void {\n try {\n applyParentDiff(worktreePath, baseCwd);\n copyParentUntrackedFiles(worktreePath, baseCwd);\n } catch (err) {\n cleanupWorktreePath(worktreePath, baseCwd);\n throw new Error(`Could not mirror parent workspace state into ${worktreePath}: ${(err as Error).message}`);\n }\n}\n\nfunction ensureWorktree(spawnId: string, baseCwd: string): string {\n mkdirSync(WORKTREES_DIR, { recursive: true });\n const hasOrphanedRun = (orphanedRunsBySpawn.get(spawnId) ?? 0) > 0;\n const worktreePath = hasOrphanedRun ? temporaryWorktreePathFor(spawnId) : worktreePathFor(spawnId);\n if (existsSync(worktreePath)) {\n rmSync(worktreePath, { recursive: true, force: true });\n }\n try {\n runGit(['worktree', 'prune'], baseCwd);\n runGit(['worktree', 'add', '--detach', worktreePath, 'HEAD'], baseCwd);\n syncParentState(worktreePath, baseCwd);\n return worktreePath;\n } catch (err) {\n logger.warn(COMPONENT, `git worktree add failed for ${worktreePath}; falling back to local clone: ${(err as Error).message}`);\n try { rmSync(worktreePath, { recursive: true, force: true }); } catch { /* best-effort */ }\n try {\n execFileSync('git', ['clone', '--local', '--no-hardlinks', baseCwd, worktreePath], {\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: 60_000,\n });\n runGit(['checkout', '--detach', 'HEAD'], worktreePath);\n syncParentState(worktreePath, baseCwd);\n return worktreePath;\n } catch (cloneErr) {\n try { rmSync(worktreePath, { recursive: true, force: true }); } catch { /* best-effort */ }\n throw new Error(`Could not create isolated worktree ${worktreePath}: ${(cloneErr as Error).message}`);\n }\n }\n}\n\nfunction cleanupWorktreePath(worktreePath: string, baseCwd: string): void {\n try {\n runGit(['worktree', 'remove', '--force', worktreePath], baseCwd);\n } catch (err) {\n logger.debug(COMPONENT, `git worktree remove skipped for ${worktreePath}: ${(err as Error).message}`);\n }\n try {\n rmSync(worktreePath, { recursive: true, force: true });\n } catch (err) {\n logger.warn(COMPONENT, `Failed to remove worktree directory ${worktreePath}: ${(err as Error).message}`);\n }\n}\n\nfunction markOrphanedRun(spawnId: string): () => void {\n orphanedRunsBySpawn.set(spawnId, (orphanedRunsBySpawn.get(spawnId) ?? 0) + 1);\n let released = false;\n return () => {\n if (released) return;\n released = true;\n const next = (orphanedRunsBySpawn.get(spawnId) ?? 1) - 1;\n if (next <= 0) {\n orphanedRunsBySpawn.delete(spawnId);\n } else {\n orphanedRunsBySpawn.set(spawnId, next);\n }\n };\n}\n\nfunction listUntrackedFiles(worktreePath: string): string[] {\n try {\n const raw = runGit(['ls-files', '--others', '--exclude-standard', '-z'], worktreePath);\n return raw.split('\\0').filter(Boolean);\n } catch (err) {\n logger.debug(COMPONENT, `Could not list untracked files in ${worktreePath}: ${(err as Error).message}`);\n return [];\n }\n}\n\nfunction diffUntrackedFile(worktreePath: string, relativePath: string): string {\n const absolutePath = resolve(worktreePath, relativePath);\n try {\n return execFileSync('git', ['diff', '--no-color', '--no-index', '--', '/dev/null', absolutePath], {\n cwd: worktreePath,\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: 60_000,\n });\n } catch (err) {\n const output = (err as { stdout?: string }).stdout;\n return typeof output === 'string' ? output : '';\n }\n}\n\n/**\n * Run a destructive tool with cwd pinned to an isolated git worktree.\n * Throws through the original execute() error after removing the\n * worktree directory so aborted spawns do not accumulate.\n */\nexport async function run(options: WorktreeRunOptions): Promise<string> {\n const baseCwd = options.baseCwd ?? process.cwd();\n const signal = options.signal;\n const worktreePath = ensureWorktree(options.spawnId, baseCwd);\n const isolatedArgs = { ...options.args, cwd: worktreePath };\n let executeSettled = false;\n let releaseOrphanedRun: (() => void) | undefined;\n let removeAbortListener: (() => void) | undefined;\n\n try {\n const executePromise = options.execute(isolatedArgs, { signal })\n .finally(() => {\n executeSettled = true;\n releaseOrphanedRun?.();\n });\n\n const abortPromise = signal\n ? new Promise<string>((_, reject) => {\n const onAbort = () => {\n if (!executeSettled) {\n releaseOrphanedRun = markOrphanedRun(options.spawnId);\n }\n cleanupWorktreePath(worktreePath, baseCwd);\n reject(signal.reason instanceof Error\n ? signal.reason\n : new Error('Isolated worktree execution aborted'));\n };\n if (signal.aborted) {\n onAbort();\n return;\n }\n signal.addEventListener('abort', onAbort, { once: true });\n removeAbortListener = () => signal.removeEventListener('abort', onAbort);\n })\n : undefined;\n\n const result = await (abortPromise\n ? Promise.race([executePromise, abortPromise])\n : executePromise);\n lastWorktreePathBySpawn.set(options.spawnId, worktreePath);\n return result;\n } catch (err) {\n cleanupWorktreePath(worktreePath, baseCwd);\n throw err;\n } finally {\n removeAbortListener?.();\n }\n}\n\n/**\n * Return a unified diff of the isolated worktree against its base\n * HEAD. Includes untracked files so newly-created artifacts are\n * visible to reviewers.\n */\nexport function getWorktreeDiff(spawnId: string): string {\n const rememberedPath = lastWorktreePathBySpawn.get(spawnId);\n const worktreePath = rememberedPath && existsSync(rememberedPath)\n ? rememberedPath\n : worktreePathFor(spawnId);\n if (!existsSync(worktreePath)) return '';\n\n let diff = '';\n try {\n diff += runGit(['diff', '--no-color', 'HEAD', '--'], worktreePath);\n } catch (err) {\n logger.warn(COMPONENT, `Could not diff tracked files in ${worktreePath}: ${(err as Error).message}`);\n }\n\n for (const relativePath of listUntrackedFiles(worktreePath)) {\n diff += diffUntrackedFile(worktreePath, relativePath);\n }\n\n return diff;\n}\n\n/** Test-only: expose the deterministic root used by Phase D.5. */\nexport function _worktreeRootForTests(): string {\n return WORKTREES_DIR;\n}\n"],"mappings":"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","names":[]}
|
package/dist/config/schema.js
CHANGED
|
@@ -91,6 +91,13 @@ const SecurityConfigSchema = z.object({
|
|
|
91
91
|
maxMemoryMB: z.number().default(2048),
|
|
92
92
|
maxSubprocesses: z.number().default(10),
|
|
93
93
|
maxDiskWriteMB: z.number().default(1024),
|
|
94
|
+
/**
|
|
95
|
+
* Phase D.5: opt-in worktree isolation for contract-declared
|
|
96
|
+
* destructive tools. Default false preserves legacy cwd behavior;
|
|
97
|
+
* enabling it routes those calls into ~/.titan/worktrees/<spawnId>
|
|
98
|
+
* so risky filesystem effects are reviewable before landing.
|
|
99
|
+
*/
|
|
100
|
+
useWorktreeIsolation: z.boolean().default(false),
|
|
94
101
|
vault: z.object({
|
|
95
102
|
enabled: z.boolean().default(false),
|
|
96
103
|
path: z.string().optional()
|
|
@@ -108,7 +115,27 @@ const SecurityConfigSchema = z.object({
|
|
|
108
115
|
}).default({}),
|
|
109
116
|
/** v5.0: Pre-execution command scanner for dangerous patterns */
|
|
110
117
|
preExecScan: z.enum(["off", "warn", "block"]).default("warn"),
|
|
111
|
-
preExecScanAllow: z.array(z.string()).default([])
|
|
118
|
+
preExecScanAllow: z.array(z.string()).default([]),
|
|
119
|
+
/**
|
|
120
|
+
* beta.17 (Phase D.4) — auto-mode classifier policy. Reads by
|
|
121
|
+
* `src/agent/autoModeClassifier.ts` to decide whether contract-
|
|
122
|
+
* declared risk levels short-circuit the approval gate, or every
|
|
123
|
+
* call still requires explicit approval.
|
|
124
|
+
*
|
|
125
|
+
* Modes:
|
|
126
|
+
* - 'standard' — (default) safe→auto, moderate→notify, high→gate
|
|
127
|
+
* - 'paranoid' — every call gates regardless of risk
|
|
128
|
+
* - 'permissive' — moderate promotes to auto; high still gates
|
|
129
|
+
* - 'yolo' — everything auto, including high. Sandbox-only.
|
|
130
|
+
* Logs a loud warning the first time it activates.
|
|
131
|
+
*
|
|
132
|
+
* User-configured `requiresApproval()` is ALWAYS checked first;
|
|
133
|
+
* a tool on the explicit approval list still gates even under
|
|
134
|
+
* 'permissive' or 'yolo'. Codex P1 #1.
|
|
135
|
+
*/
|
|
136
|
+
autoMode: z.object({
|
|
137
|
+
policy: z.enum(["standard", "paranoid", "permissive", "yolo"]).default("standard")
|
|
138
|
+
}).default({})
|
|
112
139
|
});
|
|
113
140
|
const GatewayConfigSchema = z.object({
|
|
114
141
|
host: z.string().default(DEFAULT_GATEWAY_HOST),
|