chainlesschain 0.162.95 → 0.162.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/chainlesschain.js +8 -18
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DLvvkb9x.js → AIOps-Bo9Ux24z.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BxhQTwqd.js → ActionButton-CExgXSQT.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-C4oUXzhG.js → Analytics-BNnZhRTZ.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CjzKJdv3.js → AppLayout-D9xfqFCT.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-B-MrugUs.js → Audit-Cpgu8uCw.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-ecIfEpIZ.js → Backup-CHdEJnNE.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BUcSfWnx.js → BaseInput-Cbw-PxBq.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DIc1n_Lk.js → Chat-ChHTZK_6.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BGzg7oVA.js → ChatBubbleRenderer-Dl6z6ZH7.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-BQ3Ueff-.js → Checkbox-Cj8d9bru.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CvSGLnqE.js → Codegen-C2YobW4j.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Br9IXZcE.js → Col-CxfV5FGT.js} +1 -1
- package/src/assets/web-panel/assets/{Community-tPuLOf0r.js → Community-CiwOASMj.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-vMWH73BE.js → Compact-CTg_2iKW.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Ct9z699x.js → Compliance-HMbsTmtw.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bxs1kL1y.js → Cowork-DyNDqGR2.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-QEpr2k4U.js → Cron-9-x5M7qy.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-u4xbUw6O.js → Crosschain-4msgDaYJ.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DXTEvyrW.js → DID--2Beb5Po.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-C4pZgzkg.js → Dashboard-T6nq0Lzs.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-LRuqREGe.js → Dropdown-D_Q6T10v.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-D7XI1yBM.js → EmailListRenderer-TnN21bJx.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-3y_SuAMQ.js → FamilyGuardDashboard-kky_uIij.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DbjbJJCW.js → Federation-C1GkNY4B.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-WPpMZcUw.js → FormItemContext-5-_Fc5YV.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-nNT9Tn4E.js → GenericCardRenderer-CmHiB7sr.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DqDaVzwV.js → Git-Cuci_BPI.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BquBZpcg.js → Governance-BU0cO2LD.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-RcPPYijs.js → Inference-BDFNgEcx.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-ByXfZV13.js → KnowledgeGraph-DITXUzHz.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CHT3DUcq.js → Logs-Ea-07CRd.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-Bi0HJ9pN.js → Marketplace-D6OBbRVG.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-QCBxPRRc.js → McpTools-eiKpIJMi.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-D4RlxGXn.js → Memory-M-X-T8UF.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CU_PV7e-.js → MobileBridge-xcZb9Ekv.js} +2 -2
- package/src/assets/web-panel/assets/{MobileProjects-c1CMFD8z.js → MobileProjects-DCfoTPRC.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-BezDB1-s.js → Mtc-BWfGDzD8.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CgLpiHpE.js → MtcAudit-imBuld-U.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-vx6bz-bA.js → Multisig-C2kcsq7I.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CGOagg3-.js → NLProgramming-DObvXSXP.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BcuDGYRS.js → Notes-C-6OGJMe.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-ZAGCDUpc.js → NotificationSettings-ub7Ozxci.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-B5onE0eX.js → OrderTableRenderer-CR2W89Vk.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-y_y20-5Y.js → Organization-DNA_Wty7.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BEICS31D.js → Overflow-rT4Wb4rQ.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-D_WRMzh4.js → P2P-CK-hARKv.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-C2pbtNbT.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-3aqCc_4f.js → Permissions-B8EIEqGJ.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-DT3R9g9Y.js → PersonalDataHub-BxNVNU3s.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-MiMWIhw0.js → Pipeline-CT9pbkyY.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-C3JsLuDa.js → Privacy-DhCNd2CS.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-EhHVi1ff.js → ProjectInit-Qngnx-vQ.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-D_dNEA37.js → ProjectSettings-Dfn-nQc1.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BKdiSaPR.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CPEhi2iZ.js → Providers-4o9B_n-X.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BFQpMleX.js → QuickAsk-bs1qvF30.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DzbovRC8.js → Recommend-CuEnKA2K.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-DoBeV3TD.js → Reputation-DbfvmRb2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DthhwX_B.js → Row-DvMJGAI3.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Dsf5U0lP.js → RssFeed-oJ0BZvmA.js} +2 -2
- package/src/assets/web-panel/assets/{Search-CJPB8kKl.js → Search-DnIVrY9O.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DI9MfpdD.js → Security-CIWhI_Tc.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BdG0TFGf.js → Services-DLrnpjwm.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DSYazY_B.js → Skeleton-CYmqgtVb.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BD309VTT.js → Skills-BUzCkoeu.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CaUlw4_6.js → Sla-DYRE4bnS.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CZAPkR7H.js → SpeechSettings-_6MurdOg.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CFRSowN3.js → SyncSettings-UIdnF5Li.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Di8EOluc.js → Tasks-COx2CjT8.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-B5FTziwi.js → Templates-BcHAmkZo.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Dj3XYfBg.js → Tenant-CjYQS8yZ.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CznH_rJT.js → Terminal-BmfOPcWt.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DTHzaXPt.js → TimelineRenderer-D5HuarNQ.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Bo56nuML.js → Tokens-C_fbProA.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-pyVUVBwb.js → Trigger-JPulpRx8.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Cp7jMkKN.js → Trust-CnO-062O.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-aPcZCdpz.js → UkeySign-HpNovvau.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DAv5j--w.js → VideoEditing-yMncxH7y.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DRhK7IDR.js → Wallet-Cug-t7Np.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-B2yQJuq8.js → WebAuthn--U1-NR-3.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-D98DhR54.js → WorkflowEditor-R-XvSJ3S.js} +1 -1
- package/src/assets/web-panel/assets/{chat-BXrRNi8C.js → chat-CCI1i6Rp.js} +1 -1
- package/src/assets/web-panel/assets/{colors-jQ94T_qn.js → colors-C-mqoaLt.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-KqUJLlF2.js → compact-item-De_N2iRR.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DxM3zuQW.js → createContext-DbTM6sjZ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-OcLPHSl5.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BGm946KV.js → hasIn-D0Adylyc.js} +1 -1
- package/src/assets/web-panel/assets/index-7E_mHkDU.js +1 -0
- package/src/assets/web-panel/assets/{index-Dib0FsTT.js → index-AY937BxS.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5NQai7O.js → index-B6hgy5VC.js} +1 -1
- package/src/assets/web-panel/assets/{index-LWTZAud0.js → index-B7XnpLHk.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXMZLfd8.js → index-B7gw-tDm.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTUs9t5F.js → index-BBgDgkwW.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ce-YoL4x.js → index-BMmeiw88.js} +1 -1
- package/src/assets/web-panel/assets/{index-BaU_fSK7.js → index-BU6XtnSx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMp41Q1Y.js → index-Bi7VUG0R.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbQvgx2e.js → index-BtE3cw9m.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWcd5zDG.js → index-C9ej-MF0.js} +3 -3
- package/src/assets/web-panel/assets/{index-BbS0cBU9.js → index-CIEiQl1j.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1VWL6Lc.js → index-CKgaW9E5.js} +1 -1
- package/src/assets/web-panel/assets/{index-DRzsaWQy.js → index-CY0KKR5f.js} +1 -1
- package/src/assets/web-panel/assets/index-CbV3-WlB.js +1 -0
- package/src/assets/web-panel/assets/{index-DSzb9VOG.js → index-Co1UkcFX.js} +1 -1
- package/src/assets/web-panel/assets/{index-B-2HpfWo.js → index-CzP5d5LW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DboUBf6R.js → index-D0NJ5lRi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZMzj4ay.js → index-D0gBsqTX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CghRL1dh.js → index-D5F6shtn.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cz_SFSix.js → index-DVtamtlm.js} +1 -1
- package/src/assets/web-panel/assets/{index-T4UJiTi3.js → index-DWT18hDG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CreXAr5Y.js → index-DaXb4B9P.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7Z2m28C.js → index-DjQA0Vez.js} +1 -1
- package/src/assets/web-panel/assets/{index-Crw76vIF.js → index-Ds8NJVEL.js} +1 -1
- package/src/assets/web-panel/assets/{index-mZNLT2SP.js → index-DuP-8HWu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnEZfi5D.js → index-DwLTiJ31.js} +1 -1
- package/src/assets/web-panel/assets/{index-1P0sXNhG.js → index-T3VhH8cO.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4b7KjRq.js → index-TDY1o0w3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOYcemym.js → index-TRy2Gc3V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAPk6ntc.js → index-W1OMxCmI.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dgzw6eKr.js → index-ftFRZbuc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7sBLech.js → index-iGPGaORA.js} +1 -1
- package/src/assets/web-panel/assets/{index-FSYz5aww.js → index-jhy5KQvg.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZpVYD4j.js → index-qUSZJcgw.js} +1 -1
- package/src/assets/web-panel/assets/{index-yavsyHif.js → index-w3f6lqkD.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfE3jxrT.js → index-xdMc71Kh.js} +1 -1
- package/src/assets/web-panel/assets/{index-aOLXuHpG.js → index-xhX5eg2Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGjCjsf1.js → index-zkTiW6d6.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BDoZJI7g.js → initDefaultProps-CIHqouWk.js} +1 -1
- package/src/assets/web-panel/assets/{motion-sceLjgp-.js → motion-D1iUKftk.js} +1 -1
- package/src/assets/web-panel/assets/{move-CTWQpFa7.js → move-YOpWjmjL.js} +1 -1
- package/src/assets/web-panel/assets/{omit-dr-NCrHZ.js → omit-C_1357UY.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DK-GX_Z2.js → pickAttrs-CDb4dq6o.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BIPf--18.js → placementArrow-D-OGYyhd.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-BzrWT5tV.js → responsiveObserve-hS-j69A0.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D5XhxlET.js → slide-tnjuvphU.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-6DI-k-9E.js → statusUtils-BERfMMcU.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DMw-eyn9.js → styleChecker-DKpl2K9E.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-D4oVdliM.js → useFlexGapSupport-C_sX4D3N.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-D3PPZnvy.js → useFs-DBcxC1vK.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Gh5ba4KP.js → usePersonalDataHub-CeoDf2XI.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CVEM1u4x.js → vnode-DC9Pmj_u.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-ClT_-9RZ.js → zoom-CZA58J7M.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/activitypub.js +19 -3
- package/src/commands/agent.js +25 -0
- package/src/commands/ask.js +13 -0
- package/src/commands/config.js +17 -1
- package/src/commands/cost.js +6 -1
- package/src/commands/dao.js +16 -3
- package/src/commands/dlp.js +7 -1
- package/src/commands/hub.js +13 -0
- package/src/commands/mtc.js +37 -1
- package/src/gateways/discord/discord-formatter.js +18 -1
- package/src/gateways/telegram/telegram-formatter.js +6 -3
- package/src/gateways/ws/ws-server.js +19 -1
- package/src/harness/plugin-manager.js +20 -0
- package/src/harness/worktree-isolator.js +17 -1
- package/src/lib/__tests__/model-deprecation.test.js +194 -0
- package/src/lib/a2a-protocol.js +23 -9
- package/src/lib/agent-core.js +2 -5
- package/src/lib/audit-mtc.js +40 -2
- package/src/lib/chat-core.js +141 -18
- package/src/lib/cli-anything-bridge.js +5 -0
- package/src/lib/cli-numeric.js +38 -0
- package/src/lib/config-manager.js +37 -6
- package/src/lib/cowork/ab-comparator-cli.js +6 -2
- package/src/lib/cowork-cron.js +45 -34
- package/src/lib/cowork-learning.js +31 -1
- package/src/lib/cowork-share.js +39 -3
- package/src/lib/cowork-template-marketplace.js +54 -2
- package/src/lib/cowork-workflow.js +33 -6
- package/src/lib/cross-chain-mtc.js +41 -12
- package/src/lib/crypto-manager.js +12 -2
- package/src/lib/dbevo.js +19 -8
- package/src/lib/downloader.js +24 -9
- package/src/lib/evomap-governance.js +4 -4
- package/src/lib/evomap-manager.js +54 -7
- package/src/lib/fatal-handler.js +48 -0
- package/src/lib/file-checkpoint.js +26 -0
- package/src/lib/git-integration.js +28 -0
- package/src/lib/goal-store.js +36 -10
- package/src/lib/hierarchical-memory.js +6 -0
- package/src/lib/llm-pricing.js +52 -2
- package/src/lib/mcp-oauth.js +37 -7
- package/src/lib/memory-manager.js +23 -2
- package/src/lib/model-deprecation.js +249 -0
- package/src/lib/pdh-feedback-ledger.js +131 -0
- package/src/lib/permanent-memory.js +30 -2
- package/src/lib/permission-rules.cjs +11 -3
- package/src/lib/personal-data-hub-aichat-wizard.js +33 -3
- package/src/lib/personal-data-hub-wiring.js +36 -13
- package/src/lib/project-instructions.js +10 -4
- package/src/lib/runnable-provider.js +62 -31
- package/src/lib/session-usage.js +34 -1
- package/src/lib/skill-loader.js +4 -1
- package/src/lib/skill-packs/generator.js +25 -2
- package/src/lib/stream-retry.js +56 -0
- package/src/lib/sync-credentials.js +53 -17
- package/src/lib/token-tracker.js +12 -2
- package/src/lib/with-file-lock.js +87 -0
- package/src/repl/agent-repl.js +39 -7
- package/src/repl/chat-repl.js +10 -0
- package/src/repl/session-cost.js +31 -3
- package/src/runtime/agent-core.js +474 -53
- package/src/runtime/coding-agent-contract-shared.cjs +58 -1
- package/src/runtime/coding-agent-policy.cjs +10 -0
- package/src/runtime/file-ref-expander.js +29 -1
- package/src/runtime/headless-runner.js +12 -1
- package/src/runtime/headless-stream.js +131 -5
- package/src/skills/video-editing/tools/commit.js +15 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-C8f5NbbI.js +0 -7
- package/src/assets/web-panel/assets/Projects-CXydUXp8.js +0 -1
- package/src/assets/web-panel/assets/devWarning-Bo1r5_zX.js +0 -1
- package/src/assets/web-panel/assets/index-BCU6dVQ-.js +0 -1
- package/src/assets/web-panel/assets/index-N_oOFphx.js +0 -1
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
writeFileSync,
|
|
23
23
|
readdirSync,
|
|
24
24
|
unlinkSync,
|
|
25
|
+
renameSync,
|
|
25
26
|
} from "node:fs";
|
|
26
27
|
import { join } from "node:path";
|
|
27
28
|
|
|
@@ -32,10 +33,38 @@ export const _deps = {
|
|
|
32
33
|
writeFileSync,
|
|
33
34
|
readdirSync,
|
|
34
35
|
unlinkSync,
|
|
36
|
+
renameSync,
|
|
35
37
|
// Injected at runtime by CLI to avoid eager evomap-client load
|
|
36
38
|
evomapClient: null,
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Atomically write a JSON file via _deps. A user template (<id>.json) is
|
|
43
|
+
* persistent state; a crash mid-write truncates it and listUserTemplates /
|
|
44
|
+
* loadUserTemplate's JSON.parse then drops or throws on it. Temp sibling +
|
|
45
|
+
* rename (atomic within a filesystem). Graceful-degrade to a direct write when
|
|
46
|
+
* the injected fs lacks renameSync — the real fs always has it, so production
|
|
47
|
+
* is always atomic.
|
|
48
|
+
*/
|
|
49
|
+
function _atomicWriteJson(file, data) {
|
|
50
|
+
if (typeof _deps.renameSync !== "function") {
|
|
51
|
+
_deps.writeFileSync(file, data, "utf-8");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
55
|
+
try {
|
|
56
|
+
_deps.writeFileSync(tmp, data, "utf-8");
|
|
57
|
+
_deps.renameSync(tmp, file);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
try {
|
|
60
|
+
if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
|
|
61
|
+
} catch {
|
|
62
|
+
/* best-effort temp cleanup */
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
39
68
|
const EVOMAP_CATEGORY = "cowork-template";
|
|
40
69
|
|
|
41
70
|
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
@@ -48,6 +77,26 @@ function _userTemplateFile(cwd, id) {
|
|
|
48
77
|
return join(_userTemplatesDir(cwd), `${id}.json`);
|
|
49
78
|
}
|
|
50
79
|
|
|
80
|
+
/**
|
|
81
|
+
* A template id is interpolated into a filesystem path (user-templates/<id>.json)
|
|
82
|
+
* and arrives from installed templates whose payload comes from an external hub /
|
|
83
|
+
* MCP download. A malicious id like "../../.ssh/authorized_keys" or
|
|
84
|
+
* "C:\\Windows\\..." would escape the user-templates dir and overwrite/delete
|
|
85
|
+
* arbitrary files. Restrict it to a single safe path segment.
|
|
86
|
+
*/
|
|
87
|
+
function _isSafeTemplateId(id) {
|
|
88
|
+
return (
|
|
89
|
+
typeof id === "string" &&
|
|
90
|
+
id.length > 0 &&
|
|
91
|
+
id !== "." &&
|
|
92
|
+
id !== ".." &&
|
|
93
|
+
!id.includes("/") &&
|
|
94
|
+
!id.includes("\\") &&
|
|
95
|
+
!id.includes(":") &&
|
|
96
|
+
!id.includes("\0")
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
51
100
|
// ─── Serialization ───────────────────────────────────────────────────────────
|
|
52
101
|
|
|
53
102
|
const SHARED_FIELDS = [
|
|
@@ -148,18 +197,21 @@ export function listUserTemplates(cwd) {
|
|
|
148
197
|
/** Save a template to the local user-templates directory. */
|
|
149
198
|
export function saveUserTemplate(cwd, template) {
|
|
150
199
|
if (!template?.id) throw new Error("template.id is required");
|
|
200
|
+
if (!_isSafeTemplateId(template.id)) {
|
|
201
|
+
throw new Error(`Unsafe template id (path traversal): ${template.id}`);
|
|
202
|
+
}
|
|
151
203
|
const dir = _userTemplatesDir(cwd);
|
|
152
204
|
_deps.mkdirSync(dir, { recursive: true });
|
|
153
|
-
|
|
205
|
+
_atomicWriteJson(
|
|
154
206
|
_userTemplateFile(cwd, template.id),
|
|
155
207
|
JSON.stringify(template, null, 2),
|
|
156
|
-
"utf-8",
|
|
157
208
|
);
|
|
158
209
|
return template;
|
|
159
210
|
}
|
|
160
211
|
|
|
161
212
|
/** Remove an installed user template. Returns true if removed. */
|
|
162
213
|
export function removeUserTemplate(cwd, id) {
|
|
214
|
+
if (!_isSafeTemplateId(id)) return false;
|
|
163
215
|
const file = _userTemplateFile(cwd, id);
|
|
164
216
|
if (!_deps.existsSync(file)) return false;
|
|
165
217
|
_deps.unlinkSync(file);
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
readdirSync,
|
|
30
30
|
unlinkSync,
|
|
31
31
|
appendFileSync,
|
|
32
|
+
renameSync,
|
|
32
33
|
} from "node:fs";
|
|
33
34
|
import { join } from "node:path";
|
|
34
35
|
import { evaluate as evalExpr, resolveReference } from "./workflow-expr.js";
|
|
@@ -47,6 +48,7 @@ export const _deps = {
|
|
|
47
48
|
readdirSync,
|
|
48
49
|
unlinkSync,
|
|
49
50
|
appendFileSync,
|
|
51
|
+
renameSync,
|
|
50
52
|
now: () => Date.now(),
|
|
51
53
|
runTask: null, // injected by CLI
|
|
52
54
|
// Timer seams (injectable for deterministic retry/timeout tests).
|
|
@@ -55,6 +57,32 @@ export const _deps = {
|
|
|
55
57
|
clearTimeout: (t) => clearTimeout(t),
|
|
56
58
|
};
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Atomically write a JSON file via _deps. A saved workflow definition is
|
|
62
|
+
* persistent user state; a crash mid-write truncates <id>.json and getWorkflow
|
|
63
|
+
* then fails to parse it. Temp sibling + rename (atomic within a filesystem).
|
|
64
|
+
* Graceful-degrade to a direct write when the injected fs lacks renameSync — the
|
|
65
|
+
* real fs always has it, so production is always atomic.
|
|
66
|
+
*/
|
|
67
|
+
function _atomicWriteJson(file, data) {
|
|
68
|
+
if (typeof _deps.renameSync !== "function") {
|
|
69
|
+
_deps.writeFileSync(file, data, "utf-8");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
73
|
+
try {
|
|
74
|
+
_deps.writeFileSync(tmp, data, "utf-8");
|
|
75
|
+
_deps.renameSync(tmp, file);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
try {
|
|
78
|
+
if (_deps.existsSync(tmp) && _deps.unlinkSync) _deps.unlinkSync(tmp);
|
|
79
|
+
} catch {
|
|
80
|
+
/* best-effort temp cleanup */
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
59
87
|
|
|
60
88
|
function workflowsDir(cwd) {
|
|
@@ -302,7 +330,10 @@ export function resolveForEachItems(forEach, resultsById) {
|
|
|
302
330
|
export function substituteItem(template, item) {
|
|
303
331
|
if (typeof template !== "string") return template;
|
|
304
332
|
const repl = typeof item === "string" ? item : JSON.stringify(item);
|
|
305
|
-
|
|
333
|
+
// Insert via a function so $-sequences in the item ($&, $`, $', $$ — common in
|
|
334
|
+
// JSON values / shell snippets) are placed literally rather than interpreted
|
|
335
|
+
// as String.replace patterns, which would corrupt the substituted value.
|
|
336
|
+
return template.replace(/\$\{item\}/g, () => repl);
|
|
306
337
|
}
|
|
307
338
|
|
|
308
339
|
/** Evaluate a step's `when` expression. Missing expression → always true. */
|
|
@@ -376,11 +407,7 @@ export function saveWorkflow(cwd, wf) {
|
|
|
376
407
|
if (!valid) throw new Error(`Invalid workflow: ${errors.join("; ")}`);
|
|
377
408
|
const dir = workflowsDir(cwd);
|
|
378
409
|
_deps.mkdirSync(dir, { recursive: true });
|
|
379
|
-
|
|
380
|
-
workflowFile(cwd, wf.id),
|
|
381
|
-
JSON.stringify(wf, null, 2),
|
|
382
|
-
"utf-8",
|
|
383
|
-
);
|
|
410
|
+
_atomicWriteJson(workflowFile(cwd, wf.id), JSON.stringify(wf, null, 2));
|
|
384
411
|
return wf;
|
|
385
412
|
}
|
|
386
413
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import mtcLib from "@chainlesschain/core-mtc";
|
|
22
|
+
import { withFileLock } from "./with-file-lock.js";
|
|
22
23
|
|
|
23
24
|
const {
|
|
24
25
|
assembleBatch,
|
|
@@ -95,6 +96,28 @@ function ensureDirs(dir) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Atomically write a UTF-8 file. The cross-chain MTC store holds config, trust
|
|
101
|
+
* anchors (federation security material), staged bridge ops, the batch-seq
|
|
102
|
+
* counter, and closed batches — a crash mid-write corrupts ledger/security
|
|
103
|
+
* state. Temp sibling + rename (atomic within a filesystem); temp names end in
|
|
104
|
+
* `.tmp` (never `.json`), so a hard-crash leftover is ignored by listStagedOps.
|
|
105
|
+
*/
|
|
106
|
+
function atomicWriteFileSync(filePath, data) {
|
|
107
|
+
const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
108
|
+
try {
|
|
109
|
+
fs.writeFileSync(tmp, data, "utf-8");
|
|
110
|
+
fs.renameSync(tmp, filePath);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
try {
|
|
113
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
114
|
+
} catch {
|
|
115
|
+
/* best-effort temp cleanup */
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
// ─────────────────────────────────────────────────────────────────────
|
|
99
122
|
// Namespace
|
|
100
123
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -173,7 +196,7 @@ export function saveCrossChainMtcConfig(dir, patch) {
|
|
|
173
196
|
"config.mode must be independent, federated, or light-client",
|
|
174
197
|
);
|
|
175
198
|
}
|
|
176
|
-
|
|
199
|
+
atomicWriteFileSync(configPath(dir), JSON.stringify(merged, null, 2));
|
|
177
200
|
return merged;
|
|
178
201
|
}
|
|
179
202
|
|
|
@@ -201,11 +224,7 @@ export function loadTrustAnchors(dir) {
|
|
|
201
224
|
}
|
|
202
225
|
|
|
203
226
|
function saveTrustAnchorStore(dir, store) {
|
|
204
|
-
|
|
205
|
-
trustAnchorsPath(dir),
|
|
206
|
-
JSON.stringify(store, null, 2),
|
|
207
|
-
"utf-8",
|
|
208
|
-
);
|
|
227
|
+
atomicWriteFileSync(trustAnchorsPath(dir), JSON.stringify(store, null, 2));
|
|
209
228
|
}
|
|
210
229
|
|
|
211
230
|
/**
|
|
@@ -456,7 +475,7 @@ export function stageBridgeOp(dir, op, opts = {}) {
|
|
|
456
475
|
if (fs.existsSync(file)) {
|
|
457
476
|
return { staged: false, path: file, reason: "ALREADY_STAGED" };
|
|
458
477
|
}
|
|
459
|
-
|
|
478
|
+
atomicWriteFileSync(file, JSON.stringify(op, null, 2));
|
|
460
479
|
return { staged: true, path: file };
|
|
461
480
|
}
|
|
462
481
|
|
|
@@ -498,7 +517,7 @@ function nextBatchSeq(dir, pair) {
|
|
|
498
517
|
}
|
|
499
518
|
const next = (map[pair] || 0) + 1;
|
|
500
519
|
map[pair] = next;
|
|
501
|
-
|
|
520
|
+
atomicWriteFileSync(p, JSON.stringify(map, null, 2));
|
|
502
521
|
return next;
|
|
503
522
|
}
|
|
504
523
|
|
|
@@ -514,6 +533,18 @@ function nextBatchSeq(dir, pair) {
|
|
|
514
533
|
* skipped: { reason: string } | null }}
|
|
515
534
|
*/
|
|
516
535
|
export function closeBatch(dir, opts = {}) {
|
|
536
|
+
// Serialize the whole staging-scan → batch-write → staging-cleanup section
|
|
537
|
+
// across processes. Without it, two concurrent closes both read the same
|
|
538
|
+
// staged ops, allocate the same per-pair sequence (namespace collision), and
|
|
539
|
+
// batch the same ops twice before clearing staging. Best-effort lock (never
|
|
540
|
+
// hangs); generous timeout since the section includes batch assembly+signing.
|
|
541
|
+
ensureDirs(dir);
|
|
542
|
+
return withFileLock(batchesDir(dir), () => _closeBatchImpl(dir, opts), {
|
|
543
|
+
timeoutMs: 15000,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function _closeBatchImpl(dir, opts = {}) {
|
|
517
548
|
ensureDirs(dir);
|
|
518
549
|
const cfg = loadCrossChainMtcConfig(dir);
|
|
519
550
|
const grouped = listStagedOps(dir);
|
|
@@ -546,16 +577,14 @@ export function closeBatch(dir, opts = {}) {
|
|
|
546
577
|
`${pair}-${String(seq).padStart(6, "0")}`,
|
|
547
578
|
);
|
|
548
579
|
fs.mkdirSync(outDir, { recursive: true });
|
|
549
|
-
|
|
580
|
+
atomicWriteFileSync(
|
|
550
581
|
path.join(outDir, "landmark.json"),
|
|
551
582
|
JSON.stringify(result.landmark, null, 2),
|
|
552
|
-
"utf-8",
|
|
553
583
|
);
|
|
554
584
|
result.envelopes.forEach((env, i) => {
|
|
555
|
-
|
|
585
|
+
atomicWriteFileSync(
|
|
556
586
|
path.join(outDir, `envelope-${String(i).padStart(4, "0")}.json`),
|
|
557
587
|
JSON.stringify(env, null, 2),
|
|
558
|
-
"utf-8",
|
|
559
588
|
);
|
|
560
589
|
});
|
|
561
590
|
|
|
@@ -136,14 +136,24 @@ export function decryptFile(inputPath, password, outputPath) {
|
|
|
136
136
|
* Check if a file is encrypted with our format.
|
|
137
137
|
*/
|
|
138
138
|
export function isEncryptedFile(filePath) {
|
|
139
|
+
let fd;
|
|
139
140
|
try {
|
|
140
|
-
|
|
141
|
+
fd = fs.openSync(filePath, "r");
|
|
141
142
|
const buf = Buffer.alloc(MAGIC_HEADER.length);
|
|
142
143
|
fs.readSync(fd, buf, 0, MAGIC_HEADER.length, 0);
|
|
143
|
-
fs.closeSync(fd);
|
|
144
144
|
return buf.equals(MAGIC_HEADER);
|
|
145
145
|
} catch (_err) {
|
|
146
146
|
return false;
|
|
147
|
+
} finally {
|
|
148
|
+
// Close in finally so a readSync throw (permission change mid-op, OS error)
|
|
149
|
+
// can't leak the descriptor — repeated failures would otherwise exhaust FDs.
|
|
150
|
+
if (fd !== undefined) {
|
|
151
|
+
try {
|
|
152
|
+
fs.closeSync(fd);
|
|
153
|
+
} catch {
|
|
154
|
+
/* already closed / invalid fd — nothing to release */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
147
157
|
}
|
|
148
158
|
}
|
|
149
159
|
|
package/src/lib/dbevo.js
CHANGED
|
@@ -41,6 +41,17 @@ export const SUGGESTION_TYPE = Object.freeze({
|
|
|
41
41
|
|
|
42
42
|
let _migrations = []; // registered migration definitions
|
|
43
43
|
let _migrationHistory = []; // executed migration records from DB
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compare two version strings with NUMERIC collation so "10" sorts AFTER "2".
|
|
47
|
+
* A migration framework must order numeric versions correctly; plain
|
|
48
|
+
* localeCompare()/Array.sort() is lexicographic ("10" < "2"), which would run
|
|
49
|
+
* migration v10 before v2 and corrupt the schema. Numeric collation also leaves
|
|
50
|
+
* zero-padded / date / semver-ish versions ordered as expected.
|
|
51
|
+
*/
|
|
52
|
+
function cmpVersion(a, b) {
|
|
53
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true });
|
|
54
|
+
}
|
|
44
55
|
let _queryLogs = [];
|
|
45
56
|
let _suggestions = new Map();
|
|
46
57
|
let _slowQueryThresholdMs = 100;
|
|
@@ -147,7 +158,7 @@ export function registerMigration(
|
|
|
147
158
|
});
|
|
148
159
|
|
|
149
160
|
// Keep sorted by version
|
|
150
|
-
_migrations.sort((a, b) => a.version
|
|
161
|
+
_migrations.sort((a, b) => cmpVersion(a.version, b.version));
|
|
151
162
|
|
|
152
163
|
return { registered: true, version, checksum };
|
|
153
164
|
}
|
|
@@ -178,13 +189,13 @@ export function getCurrentVersion(db) {
|
|
|
178
189
|
);
|
|
179
190
|
|
|
180
191
|
if (active.length === 0) return null;
|
|
181
|
-
return active.sort((a, b) => b
|
|
192
|
+
return active.sort((a, b) => cmpVersion(b, a))[0];
|
|
182
193
|
}
|
|
183
194
|
|
|
184
195
|
export function getPendingMigrations(db) {
|
|
185
196
|
const current = getCurrentVersion(db);
|
|
186
197
|
return _migrations.filter(
|
|
187
|
-
(m) => !current || m.version
|
|
198
|
+
(m) => !current || cmpVersion(m.version, current) > 0,
|
|
188
199
|
);
|
|
189
200
|
}
|
|
190
201
|
|
|
@@ -193,7 +204,7 @@ export function migrateUp(db, targetVersion) {
|
|
|
193
204
|
if (pending.length === 0) return { migrated: false, reason: "no_pending" };
|
|
194
205
|
|
|
195
206
|
const toRun = targetVersion
|
|
196
|
-
? pending.filter((m) => m.version
|
|
207
|
+
? pending.filter((m) => cmpVersion(m.version, targetVersion) <= 0)
|
|
197
208
|
: pending;
|
|
198
209
|
|
|
199
210
|
if (toRun.length === 0) return { migrated: false, reason: "no_pending" };
|
|
@@ -248,13 +259,13 @@ export function migrateDown(db, targetVersion) {
|
|
|
248
259
|
.filter((m) => {
|
|
249
260
|
if (targetVersion) {
|
|
250
261
|
return (
|
|
251
|
-
m.version
|
|
252
|
-
m.version
|
|
262
|
+
cmpVersion(m.version, current) <= 0 &&
|
|
263
|
+
cmpVersion(m.version, targetVersion) > 0
|
|
253
264
|
);
|
|
254
265
|
}
|
|
255
266
|
return m.version === current;
|
|
256
267
|
})
|
|
257
|
-
.sort((a, b) => b.version
|
|
268
|
+
.sort((a, b) => cmpVersion(b.version, a.version));
|
|
258
269
|
|
|
259
270
|
if (toRollBack.length === 0)
|
|
260
271
|
return { rolledBack: false, reason: "nothing_to_rollback" };
|
|
@@ -331,7 +342,7 @@ export function validateMigrations() {
|
|
|
331
342
|
if (_migrations.length === 0) return { valid: true, issues: [] };
|
|
332
343
|
|
|
333
344
|
const issues = [];
|
|
334
|
-
const versions = _migrations.map((m) => m.version).sort();
|
|
345
|
+
const versions = _migrations.map((m) => m.version).sort(cmpVersion);
|
|
335
346
|
|
|
336
347
|
// Check for gaps in version sequence
|
|
337
348
|
for (let i = 1; i < versions.length; i++) {
|
package/src/lib/downloader.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { pipeline } from "node:stream/promises";
|
|
10
|
-
import {
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
11
|
import ora from "ora";
|
|
12
12
|
import { GITHUB_RELEASES_URL } from "../constants.js";
|
|
13
13
|
import { getBinDir, ensureDir } from "./paths.js";
|
|
@@ -208,16 +208,31 @@ async function fetchRelease(url) {
|
|
|
208
208
|
return response.json();
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
function extractZip(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
211
|
+
export function extractZip(
|
|
212
|
+
zipPath,
|
|
213
|
+
destDir,
|
|
214
|
+
{ execFileImpl = execFileSync, platform = getPlatform() } = {},
|
|
215
|
+
) {
|
|
216
|
+
// Never interpolate the paths into a shell string — `zipPath`/`destDir` derive
|
|
217
|
+
// from the (remote) release asset name, so a `"`/`$()`/`'` in a path would be a
|
|
218
|
+
// command-injection vector. execFileSync runs the tool directly (no shell), and
|
|
219
|
+
// on Windows the paths go through the environment so the -Command script stays
|
|
220
|
+
// a constant the shell can't be tricked by.
|
|
221
|
+
if (platform === "win32") {
|
|
222
|
+
execFileImpl(
|
|
223
|
+
"powershell",
|
|
224
|
+
[
|
|
225
|
+
"-NoProfile",
|
|
226
|
+
"-Command",
|
|
227
|
+
"Expand-Archive -Path $env:CC_ZIP_SRC -DestinationPath $env:CC_ZIP_DEST -Force",
|
|
228
|
+
],
|
|
229
|
+
{
|
|
230
|
+
stdio: "ignore",
|
|
231
|
+
env: { ...process.env, CC_ZIP_SRC: zipPath, CC_ZIP_DEST: destDir },
|
|
232
|
+
},
|
|
218
233
|
);
|
|
219
234
|
} else {
|
|
220
|
-
|
|
235
|
+
execFileImpl("unzip", ["-o", zipPath, "-d", destDir], { stdio: "ignore" });
|
|
221
236
|
}
|
|
222
237
|
}
|
|
223
238
|
|
|
@@ -305,8 +305,8 @@ export function createGovernanceProposalV2(db, options = {}) {
|
|
|
305
305
|
throw new Error(`Unknown proposal type: ${proposalType}`);
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
const quorumValue =
|
|
309
|
-
const thresholdValue =
|
|
308
|
+
const quorumValue = Number.isFinite(quorum) ? quorum : 3;
|
|
309
|
+
const thresholdValue = Number.isFinite(threshold) ? threshold : 0.5;
|
|
310
310
|
if (quorumValue < 1) throw new Error("Quorum must be >= 1");
|
|
311
311
|
if (thresholdValue <= 0 || thresholdValue > 1) {
|
|
312
312
|
throw new Error("Threshold must be in (0, 1]");
|
|
@@ -370,7 +370,7 @@ export function castVoteV2(db, options = {}) {
|
|
|
370
370
|
if (!Object.values(VOTE_DIRECTION).includes(direction)) {
|
|
371
371
|
throw new Error(`Unknown vote direction: ${direction}`);
|
|
372
372
|
}
|
|
373
|
-
const weightValue =
|
|
373
|
+
const weightValue = Number.isFinite(weight) ? weight : 1;
|
|
374
374
|
if (weightValue <= 0) throw new Error("Vote weight must be positive");
|
|
375
375
|
|
|
376
376
|
if (direction === VOTE_DIRECTION.FOR) {
|
|
@@ -456,7 +456,7 @@ export function cancelProposal(db, proposalId) {
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
export function expireProposalsV2(db, now) {
|
|
459
|
-
const cutoff =
|
|
459
|
+
const cutoff = Number.isFinite(now) ? now : Date.now();
|
|
460
460
|
const expired = [];
|
|
461
461
|
for (const p of _proposals.values()) {
|
|
462
462
|
if (p.status === PROPOSAL_STATUS_V2.ACTIVE) {
|
|
@@ -14,6 +14,53 @@ export const _deps = {
|
|
|
14
14
|
path,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Atomically write a UTF-8 file via _deps.fs. A gene's gene.json / content.md
|
|
19
|
+
* are persistent store state; a crash mid-write truncates them and getGene's
|
|
20
|
+
* JSON.parse then throws. Temp sibling + rename (atomic within a filesystem).
|
|
21
|
+
* Graceful-degrade to a direct write when the injected fs has no renameSync
|
|
22
|
+
* (partial test mocks); the real fs always has it, so production is atomic.
|
|
23
|
+
*/
|
|
24
|
+
function _atomicWriteFile(filePath, data) {
|
|
25
|
+
const fsx = _deps.fs;
|
|
26
|
+
if (typeof fsx.renameSync !== "function") {
|
|
27
|
+
fsx.writeFileSync(filePath, data, "utf-8");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const tmp = `${filePath}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
31
|
+
try {
|
|
32
|
+
fsx.writeFileSync(tmp, data, "utf-8");
|
|
33
|
+
fsx.renameSync(tmp, filePath);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
try {
|
|
36
|
+
if (fsx.existsSync(tmp) && fsx.unlinkSync) fsx.unlinkSync(tmp);
|
|
37
|
+
} catch {
|
|
38
|
+
/* best-effort temp cleanup */
|
|
39
|
+
}
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A gene id is interpolated into a filesystem path (genesDir/<id>/...). Gene ids
|
|
46
|
+
* arrive from downloaded/installed genes (an external hub / MCP response), so a
|
|
47
|
+
* malicious `id` like "../../.ssh/authorized_keys" or "C:\\Windows\\..." would
|
|
48
|
+
* escape genesDir and read/overwrite/delete arbitrary files. Restrict it to a
|
|
49
|
+
* single safe path segment (no separators, no traversal, no drive/NUL).
|
|
50
|
+
*/
|
|
51
|
+
function _isSafeGeneId(id) {
|
|
52
|
+
return (
|
|
53
|
+
typeof id === "string" &&
|
|
54
|
+
id.length > 0 &&
|
|
55
|
+
id !== "." &&
|
|
56
|
+
id !== ".." &&
|
|
57
|
+
!id.includes("/") &&
|
|
58
|
+
!id.includes("\\") &&
|
|
59
|
+
!id.includes(":") &&
|
|
60
|
+
!id.includes("\0")
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
17
64
|
export class EvoMapManager {
|
|
18
65
|
/**
|
|
19
66
|
* @param {object} options
|
|
@@ -71,6 +118,9 @@ export class EvoMapManager {
|
|
|
71
118
|
saveGene(gene, content) {
|
|
72
119
|
this.initialize();
|
|
73
120
|
if (!gene || !gene.id) throw new Error("Gene ID required");
|
|
121
|
+
if (!_isSafeGeneId(gene.id)) {
|
|
122
|
+
throw new Error(`Unsafe gene id (path traversal): ${gene.id}`);
|
|
123
|
+
}
|
|
74
124
|
|
|
75
125
|
const geneDir = _deps.path.join(this.genesDir, gene.id);
|
|
76
126
|
if (!_deps.fs.existsSync(geneDir)) {
|
|
@@ -78,19 +128,14 @@ export class EvoMapManager {
|
|
|
78
128
|
}
|
|
79
129
|
|
|
80
130
|
// Save metadata
|
|
81
|
-
|
|
131
|
+
_atomicWriteFile(
|
|
82
132
|
_deps.path.join(geneDir, "gene.json"),
|
|
83
133
|
JSON.stringify(gene, null, 2),
|
|
84
|
-
"utf-8",
|
|
85
134
|
);
|
|
86
135
|
|
|
87
136
|
// Save content
|
|
88
137
|
if (content) {
|
|
89
|
-
_deps.
|
|
90
|
-
_deps.path.join(geneDir, "content.md"),
|
|
91
|
-
content,
|
|
92
|
-
"utf-8",
|
|
93
|
-
);
|
|
138
|
+
_atomicWriteFile(_deps.path.join(geneDir, "content.md"), content);
|
|
94
139
|
}
|
|
95
140
|
|
|
96
141
|
// Register in DB
|
|
@@ -170,6 +215,7 @@ export class EvoMapManager {
|
|
|
170
215
|
*/
|
|
171
216
|
getGene(geneId) {
|
|
172
217
|
this.initialize();
|
|
218
|
+
if (!_isSafeGeneId(geneId)) return null;
|
|
173
219
|
const geneDir = _deps.path.join(this.genesDir, geneId);
|
|
174
220
|
|
|
175
221
|
if (!_deps.fs.existsSync(geneDir)) return null;
|
|
@@ -192,6 +238,7 @@ export class EvoMapManager {
|
|
|
192
238
|
*/
|
|
193
239
|
removeGene(geneId) {
|
|
194
240
|
this.initialize();
|
|
241
|
+
if (!_isSafeGeneId(geneId)) return { removed: false };
|
|
195
242
|
const geneDir = _deps.path.join(this.genesDir, geneId);
|
|
196
243
|
|
|
197
244
|
if (_deps.fs.existsSync(geneDir)) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized friendly-error boundary for the CLI entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* An uncaught error thrown from a command action (malformed --json, a failed DB
|
|
5
|
+
* op) — or, crucially, one that escapes into an async event handler / detached
|
|
6
|
+
* task / third-party lib AFTER the top-level parse — otherwise surfaces as a raw
|
|
7
|
+
* stack trace via Node's default unhandledRejection/uncaughtException, which on
|
|
8
|
+
* modern Node terminates the process. Route all of those through one place that
|
|
9
|
+
* prints a clean `error: <message>` (full stack under --verbose or
|
|
10
|
+
* CC_DEBUG/DEBUG) and exits non-zero.
|
|
11
|
+
*
|
|
12
|
+
* Pure-ish: process bindings are injectable so this is unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export function reportFatal(
|
|
16
|
+
err,
|
|
17
|
+
{
|
|
18
|
+
stderr = process.stderr,
|
|
19
|
+
exit = process.exit,
|
|
20
|
+
argv = process.argv,
|
|
21
|
+
env = process.env,
|
|
22
|
+
} = {},
|
|
23
|
+
) {
|
|
24
|
+
const verbose =
|
|
25
|
+
argv.includes("--verbose") || Boolean(env.CC_DEBUG) || Boolean(env.DEBUG);
|
|
26
|
+
if (verbose && err && err.stack) {
|
|
27
|
+
stderr.write(`${err.stack}\n`);
|
|
28
|
+
} else {
|
|
29
|
+
const msg = err && err.message ? err.message : String(err);
|
|
30
|
+
stderr.write(`error: ${msg}\n`);
|
|
31
|
+
}
|
|
32
|
+
exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Install process-level safety nets that funnel unhandled rejections and
|
|
37
|
+
* uncaught exceptions through `report`. Node terminates on these by default, so
|
|
38
|
+
* this only swaps the ugly default stack for the friendly boundary — it is not
|
|
39
|
+
* a behavior change (still exits non-zero).
|
|
40
|
+
*/
|
|
41
|
+
export function installGlobalErrorHandlers(proc = process, report = reportFatal) {
|
|
42
|
+
proc.on("unhandledRejection", (reason) =>
|
|
43
|
+
report(reason instanceof Error ? reason : new Error(String(reason))),
|
|
44
|
+
);
|
|
45
|
+
proc.on("uncaughtException", (err) =>
|
|
46
|
+
report(err instanceof Error ? err : new Error(String(err))),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -80,6 +80,27 @@ function newId() {
|
|
|
80
80
|
return `cp-${Date.now()}-${rand}`;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* A checkpoint id is interpolated into filesystem paths (root/<id>.json and the
|
|
85
|
+
* blob dir root/<id>). The id comes from CLI args (`cc checkpoint show|delete
|
|
86
|
+
* <id>`), so an id like "../../etc/passwd" would read outside the checkpoint
|
|
87
|
+
* store, and "../../important" passed to delete would rmSync outside it. The git
|
|
88
|
+
* engine guards this via sanitizeSession(); mirror it for the copy fallback by
|
|
89
|
+
* restricting the id to a single safe path segment.
|
|
90
|
+
*/
|
|
91
|
+
function isSafeCheckpointId(id) {
|
|
92
|
+
return (
|
|
93
|
+
typeof id === "string" &&
|
|
94
|
+
id.length > 0 &&
|
|
95
|
+
id !== "." &&
|
|
96
|
+
id !== ".." &&
|
|
97
|
+
!id.includes("/") &&
|
|
98
|
+
!id.includes("\\") &&
|
|
99
|
+
!id.includes(":") &&
|
|
100
|
+
!id.includes("\0")
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
83
104
|
/**
|
|
84
105
|
* Recursively collect regular files under an absolute path, honoring SKIP_DIRS
|
|
85
106
|
* and a running maxFiles budget. Symlinks are not followed.
|
|
@@ -146,6 +167,9 @@ export function createCheckpoint(paths, opts = {}) {
|
|
|
146
167
|
const uniqueAbs = [...new Set(absFiles)];
|
|
147
168
|
|
|
148
169
|
const id = opts.id || newId();
|
|
170
|
+
if (!isSafeCheckpointId(id)) {
|
|
171
|
+
throw new Error(`Unsafe checkpoint id (path traversal): ${id}`);
|
|
172
|
+
}
|
|
149
173
|
const blobDir = path.join(root, id);
|
|
150
174
|
ensureDir(blobDir);
|
|
151
175
|
|
|
@@ -181,6 +205,7 @@ export function createCheckpoint(paths, opts = {}) {
|
|
|
181
205
|
|
|
182
206
|
/** Load a checkpoint manifest by id, or null. */
|
|
183
207
|
export function getCheckpoint(id, opts = {}) {
|
|
208
|
+
if (!isSafeCheckpointId(id)) return null;
|
|
184
209
|
const root = opts.root || defaultRoot();
|
|
185
210
|
const file = path.join(root, `${id}.json`);
|
|
186
211
|
if (!fs.existsSync(file)) return null;
|
|
@@ -310,6 +335,7 @@ export function restoreCheckpoint(id, opts = {}) {
|
|
|
310
335
|
|
|
311
336
|
/** Delete a checkpoint (manifest + blobs). Returns true if it existed. */
|
|
312
337
|
export function deleteCheckpoint(id, opts = {}) {
|
|
338
|
+
if (!isSafeCheckpointId(id)) return false;
|
|
313
339
|
const root = opts.root || defaultRoot();
|
|
314
340
|
const file = path.join(root, `${id}.json`);
|
|
315
341
|
const blobDir = path.join(root, id);
|