chainlesschain 0.162.162 → 0.162.164
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/package.json +3 -1
- package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BqRkaHGb.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-DWO7OZLC.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-C5oaVezM.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-CsUESDoZ.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-Dragqu5k.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-CQsaHctY.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-DdG1jNSM.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-DN6sjK4z.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-Cxm_ZTCl.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-DVXGmA56.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-HwJR-C6B.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DEc4vD_A.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-CKiTcv36.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-Br9UUNN0.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-NeUH_70T.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-BqR-z2Df.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DcWB_SgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BfhgcudF.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-LaU29LgD.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-B2ZB9uwW.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BuHl38A2.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-m627yqvZ.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-C7bxF8F5.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DPLPQzLp.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-Bvkzg88z.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-BTsiA7h_.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-DS_j6r25.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CKRG3Rsl.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-DW8INoNQ.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-wqi0uxuR.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-CMlTuPLT.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-CSx4m5JJ.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-vnQCUy-_.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-BV2NA742.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-CzK_FPB1.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-Cb7G9oL3.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-qJxlUyzr.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-DqaBJ7Te.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-CwhTJkRi.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-YDs4Fal4.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-CEmQm9n8.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-zby_P_LA.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-DsmDNFNT.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-DuzQ6me5.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-DZbeN613.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CoNSVAUS.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-OYJEAaMC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-CWIapV--.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-CX8xMxZn.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-BLCN8YyY.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-BV_sxp75.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-BTnRrHRp.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-D1KZg018.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-COm5cuNg.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-0saWEkYI.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-Dr7iKLS2.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-C1vpPBcr.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-CxlWrtNj.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-BT2V8dPN.js} +1 -1
- package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-bm9V3c5k.js} +4 -4
- package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CocH1yyX.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DHItlMdd.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-C59sLc4w.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Crj4BMc8.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-DX4KFceY.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-BD_iwQLr.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-BmCTzYsH.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-BGUaxZYl.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-BS0tJx2O.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal--tHSAsld.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D9-MFxS4.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-CiCB485f.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-SmCFuW_l.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-DkK3Rqe0.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-CDM4LGdB.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-IDB7DJXt.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-Bikj5cmt.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-C9abWlAw.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-DrbRKVky.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-CccCSDQf.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-CXObXLhb.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-B5kfYAxC.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-DmiPBfip.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-Q6lYteAc.js} +1 -1
- package/src/assets/web-panel/assets/{index-C229ABfy.js → index-25Wlqps4.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-33U29UNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-4oXBDczR.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-BGYXvMc5.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-BJCCa0s_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-BJJ5DhKI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-BLOzViJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-BRrbUMoG.js} +1 -1
- package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BUFpT17Q.js} +1 -1
- package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BkRDiqGc.js} +1 -1
- package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-BsfvW6my.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-By-kfPED.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-BzFTcQsc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-C2FXdboq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-C5XOlTHN.js} +1 -1
- package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-CLhBDS2w.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-CMq7UeoT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-CVMTWNKl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-Caghi-yL.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CdB6FuSH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-CoK8-kWQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-CsO3GUAY.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-CwZBa20Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-D-kASucw.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbepULSc.js → index-D8eBw7vW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D9wgifk4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-DAYCTc07.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-DF6Q8l2h.js} +1 -1
- package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
- package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-DgTLApS9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-DgxVJ8pg.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-DlJvIqoR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-Dw4eHfsG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-Rh-ytwow.js} +3 -3
- package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-StAEt2pq.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-bzfw951k.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-fSUMbMvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-tFHy5Wol.js} +1 -1
- package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
- package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-BNq7X72N.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-DOf9viTc.js} +1 -1
- package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CRHTcW4x.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-Cyoozgm2.js} +1 -1
- package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-6-TjFNru.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-DlUmO1zi.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-CFOL8mIR.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BtaFK5zd.js} +1 -1
- package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BIbc2bur.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-BXX9d6-w.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-BGv10Bpn.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-FY4IL4jn.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-BM3OHxAT.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-D1fbFiHB.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +320 -32
- package/src/commands/agent.js +105 -11
- package/src/commands/context.js +125 -14
- package/src/commands/doctor.js +85 -1
- package/src/commands/hook.js +181 -7
- package/src/commands/plugin.js +456 -1
- package/src/commands/review.js +363 -2
- package/src/commands/session.js +191 -0
- package/src/commands/team.js +19 -1
- package/src/data/changelog.json +10 -4
- package/src/gateways/ws/remote-session-protocol.js +39 -0
- package/src/gateways/ws/session-protocol.js +55 -2
- package/src/gateways/ws/ws-agent-handler.js +50 -0
- package/src/harness/jsonl-session-store.js +60 -0
- package/src/harness/worktree-isolator.js +140 -5
- package/src/lib/agent-schedule-store.js +318 -10
- package/src/lib/agent-team/team-worktree.js +37 -2
- package/src/lib/agent-tool-admission.js +125 -0
- package/src/lib/app-preview.js +360 -0
- package/src/lib/async-hook-supervisor.cjs +54 -0
- package/src/lib/capability-manifest.js +319 -0
- package/src/lib/capability-negotiation.js +23 -12
- package/src/lib/channels/channel-manager.js +54 -2
- package/src/lib/cli-context-engineering.js +9 -2
- package/src/lib/context-breakdown.js +116 -0
- package/src/lib/diagnostic-bundle.js +230 -0
- package/src/lib/doctor-checkup.js +275 -4
- package/src/lib/execution-location.js +286 -0
- package/src/lib/headless-manifest.js +13 -40
- package/src/lib/hook-event-log.cjs +193 -0
- package/src/lib/hook-stats-store.cjs +197 -0
- package/src/lib/idempotency.js +109 -0
- package/src/lib/interaction-adapter.js +34 -4
- package/src/lib/json-schema-output.js +46 -12
- package/src/lib/json-schema-validate.js +143 -3
- package/src/lib/jsonl-session-store.js +1 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
- package/src/lib/lsp/lsp-manager.js +56 -0
- package/src/lib/monitor-event.js +290 -0
- package/src/lib/operation-fingerprint.js +217 -0
- package/src/lib/plugin-runtime/bin.js +37 -0
- package/src/lib/plugin-runtime/capabilities.js +19 -0
- package/src/lib/plugin-runtime/capability-consent.js +203 -0
- package/src/lib/plugin-runtime/hooks.js +37 -0
- package/src/lib/plugin-runtime/lsp.js +38 -0
- package/src/lib/plugin-runtime/mcp.js +40 -0
- package/src/lib/plugin-runtime/monitors.js +37 -0
- package/src/lib/plugin-runtime/plugin-options.js +151 -0
- package/src/lib/plugin-runtime/policy.js +62 -0
- package/src/lib/plugin-runtime/scopes.js +20 -4
- package/src/lib/pr-automation-policy.js +224 -0
- package/src/lib/project-instructions.js +128 -0
- package/src/lib/remote-approval-bridge.js +66 -13
- package/src/lib/repl-goal.js +132 -0
- package/src/lib/repl-rewind.js +153 -0
- package/src/lib/review-comment-anchor.js +241 -0
- package/src/lib/review-pipeline.js +46 -7
- package/src/lib/runtime-checkup.js +221 -0
- package/src/lib/schedule-planner.js +17 -1
- package/src/lib/secret-scan.js +409 -0
- package/src/lib/session-branch.js +0 -0
- package/src/lib/settings-hook-events.cjs +13 -0
- package/src/lib/settings-loader.cjs +42 -0
- package/src/lib/side-effect-ledger-store.js +88 -0
- package/src/lib/side-effect-ledger.js +401 -0
- package/src/lib/skill-loader.js +32 -0
- package/src/lib/skill-path-scope.js +63 -0
- package/src/lib/sub-agent-context.js +110 -9
- package/src/lib/sub-agent-registry.js +19 -0
- package/src/lib/subagent-inheritance.js +112 -0
- package/src/lib/telemetry/span-recorder.js +17 -4
- package/src/lib/terminal-context.js +195 -0
- package/src/lib/turn-binding-store.js +119 -0
- package/src/lib/unattended-action-policy.js +182 -0
- package/src/lib/worktree-cleanup-safety.js +138 -0
- package/src/lib/worktree-isolator.js +8 -0
- package/src/lib/worktree-sparse.js +131 -0
- package/src/repl/agent-repl.js +280 -30
- package/src/repl/permission-tier.js +28 -0
- package/src/runtime/agent-core.js +565 -21
- package/src/runtime/coding-agent-contract-shared.cjs +60 -1
- package/src/runtime/diagnostics.js +60 -0
- package/src/runtime/headless-runner.js +393 -1
- package/src/runtime/headless-stream.js +60 -1
- package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +0 -1
- package/src/assets/web-panel/assets/index-B7a1TrU6.js +0 -1
- package/src/assets/web-panel/assets/index-C2B1KRja.js +0 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hook-stats-store — a small, best-effort, cross-session RELIABILITY aggregate
|
|
5
|
+
* for async settings hooks, so `cc doctor` (a separate process) can surface
|
|
6
|
+
* hooks that have been slow, repeatedly failing, or circuit-broken. The gap's
|
|
7
|
+
* P2 "Doctor" wants "慢/熔断 Hook" health, but that state otherwise lives only
|
|
8
|
+
* in-memory in AsyncHookSupervisor for the life of one process.
|
|
9
|
+
*
|
|
10
|
+
* Design: the supervisor folds each completed hook run into an in-memory
|
|
11
|
+
* aggregate (bounded by the number of DISTINCT hooks, not runs) and persists it
|
|
12
|
+
* once on stopAll() — NOT per run — so there is no write amplification on the
|
|
13
|
+
* hook hot path. Persistence merges with whatever is already on disk, so counts
|
|
14
|
+
* accumulate across sessions. Reads/writes are best-effort (a corrupt/ío-failed
|
|
15
|
+
* file never blocks a hook or the agent).
|
|
16
|
+
*
|
|
17
|
+
* Pure aggregation (no clock/RNG/fs in the fold) so it's fully unit-testable;
|
|
18
|
+
* load/save take an injected fs. The default circuit threshold mirrors the
|
|
19
|
+
* doctor evaluator's `hookFailureThreshold`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const os = require("node:os");
|
|
23
|
+
const path = require("node:path");
|
|
24
|
+
const fsDefault = require("node:fs");
|
|
25
|
+
|
|
26
|
+
const MAX_HOOKS = 200; // bound distinct-hook entries; drop least-recently-run
|
|
27
|
+
const DEFAULT_CIRCUIT_THRESHOLD = 3;
|
|
28
|
+
const CONFIG_DIR_NAME = ".chainlesschain";
|
|
29
|
+
|
|
30
|
+
/** Stable per-hook id from its (event, command). */
|
|
31
|
+
function hookKey(command, event) {
|
|
32
|
+
return `${event || "*"}::${String(command || "").trim()}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Default on-disk location (matches the ESM getHomeDir() layout). */
|
|
36
|
+
function defaultHookStatsPath() {
|
|
37
|
+
return path.join(os.homedir(), CONFIG_DIR_NAME, "hook-stats.json");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fresh empty aggregate map. */
|
|
41
|
+
function emptyStats() {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fold ONE completed run into the aggregate (mutates + returns `stats`). Pure:
|
|
47
|
+
* `now`/`ok`/`ms` are supplied by the caller. `consecutiveFailures` resets on
|
|
48
|
+
* any success — the circuit signal is "N failures in a row", not lifetime.
|
|
49
|
+
*/
|
|
50
|
+
function aggregateRun(stats, { command, event, ok, ms, now }) {
|
|
51
|
+
if (!stats || typeof stats !== "object") stats = {};
|
|
52
|
+
const id = hookKey(command, event);
|
|
53
|
+
const dur = Number.isFinite(Number(ms)) ? Number(ms) : 0;
|
|
54
|
+
const at = Number.isFinite(Number(now)) ? Number(now) : 0;
|
|
55
|
+
const e = stats[id] || {
|
|
56
|
+
id,
|
|
57
|
+
command: command || "",
|
|
58
|
+
event: event || null,
|
|
59
|
+
runs: 0,
|
|
60
|
+
failures: 0,
|
|
61
|
+
consecutiveFailures: 0,
|
|
62
|
+
lastFailureAt: null,
|
|
63
|
+
totalMs: 0,
|
|
64
|
+
maxMs: 0,
|
|
65
|
+
lastRunAt: 0,
|
|
66
|
+
};
|
|
67
|
+
e.runs += 1;
|
|
68
|
+
e.totalMs += dur;
|
|
69
|
+
if (dur > e.maxMs) e.maxMs = dur;
|
|
70
|
+
if (at > e.lastRunAt) e.lastRunAt = at;
|
|
71
|
+
if (ok === true) {
|
|
72
|
+
e.consecutiveFailures = 0;
|
|
73
|
+
} else {
|
|
74
|
+
e.failures += 1;
|
|
75
|
+
e.consecutiveFailures += 1;
|
|
76
|
+
e.lastFailureAt = at;
|
|
77
|
+
}
|
|
78
|
+
stats[id] = e;
|
|
79
|
+
boundStats(stats);
|
|
80
|
+
return stats;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Keep only the MAX_HOOKS most-recently-run entries. */
|
|
84
|
+
function boundStats(stats) {
|
|
85
|
+
const keys = Object.keys(stats);
|
|
86
|
+
if (keys.length <= MAX_HOOKS) return stats;
|
|
87
|
+
keys
|
|
88
|
+
.sort((a, b) => (stats[a].lastRunAt || 0) - (stats[b].lastRunAt || 0))
|
|
89
|
+
.slice(0, keys.length - MAX_HOOKS)
|
|
90
|
+
.forEach((k) => delete stats[k]);
|
|
91
|
+
return stats;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Merge a session `delta` aggregate INTO a `base` aggregate (returns a new map).
|
|
96
|
+
* Counts add; maxMs/lastRunAt/lastFailureAt take the larger; the session's
|
|
97
|
+
* consecutiveFailures wins (it reflects the most recent streak).
|
|
98
|
+
*/
|
|
99
|
+
function mergeStats(base, delta) {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const src of [base, delta]) {
|
|
102
|
+
if (!src || typeof src !== "object") continue;
|
|
103
|
+
for (const [id, e] of Object.entries(src)) {
|
|
104
|
+
if (!e || typeof e !== "object") continue;
|
|
105
|
+
const cur = out[id];
|
|
106
|
+
if (!cur) {
|
|
107
|
+
out[id] = { ...e };
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
cur.runs += e.runs || 0;
|
|
111
|
+
cur.failures += e.failures || 0;
|
|
112
|
+
cur.totalMs += e.totalMs || 0;
|
|
113
|
+
cur.maxMs = Math.max(cur.maxMs || 0, e.maxMs || 0);
|
|
114
|
+
cur.lastRunAt = Math.max(cur.lastRunAt || 0, e.lastRunAt || 0);
|
|
115
|
+
cur.lastFailureAt =
|
|
116
|
+
Math.max(cur.lastFailureAt || 0, e.lastFailureAt || 0) || null;
|
|
117
|
+
// `delta` is applied second, so its streak (most recent) wins.
|
|
118
|
+
cur.consecutiveFailures =
|
|
119
|
+
e.consecutiveFailures ?? cur.consecutiveFailures;
|
|
120
|
+
cur.command = e.command || cur.command;
|
|
121
|
+
cur.event = e.event ?? cur.event;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return boundStats(out);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Shape the aggregate for the doctor `checkHooks` evaluator: id, lifetime
|
|
129
|
+
* failure count, average duration, and a derived circuit-open flag.
|
|
130
|
+
*/
|
|
131
|
+
function toCheckHooksInput(
|
|
132
|
+
stats,
|
|
133
|
+
{ circuitThreshold = DEFAULT_CIRCUIT_THRESHOLD } = {},
|
|
134
|
+
) {
|
|
135
|
+
if (!stats || typeof stats !== "object") return [];
|
|
136
|
+
return Object.values(stats).map((e) => ({
|
|
137
|
+
id: e.command || e.id,
|
|
138
|
+
event: e.event || null,
|
|
139
|
+
failures: e.failures || 0,
|
|
140
|
+
avgMs: e.runs > 0 ? e.totalMs / e.runs : 0,
|
|
141
|
+
circuitOpen: (e.consecutiveFailures || 0) >= circuitThreshold,
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Best-effort read of the on-disk aggregate (→ {} on missing/corrupt). */
|
|
146
|
+
function loadHookStats(file = defaultHookStatsPath(), fs = fsDefault) {
|
|
147
|
+
try {
|
|
148
|
+
if (!fs.existsSync(file)) return {};
|
|
149
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
150
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
151
|
+
} catch {
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Best-effort atomic write (tmp + rename). Never throws. */
|
|
157
|
+
function saveHookStats(stats, file = defaultHookStatsPath(), fs = fsDefault) {
|
|
158
|
+
try {
|
|
159
|
+
const dir = path.dirname(file);
|
|
160
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
161
|
+
const tmp = `${file}.tmp`;
|
|
162
|
+
fs.writeFileSync(tmp, JSON.stringify(stats), "utf-8");
|
|
163
|
+
fs.renameSync(tmp, file);
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Load the on-disk aggregate, fold in a session `delta`, and write it back.
|
|
172
|
+
* Best-effort end-to-end — the single call a supervisor makes on stopAll().
|
|
173
|
+
*/
|
|
174
|
+
function persistSessionStats(
|
|
175
|
+
delta,
|
|
176
|
+
file = defaultHookStatsPath(),
|
|
177
|
+
fs = fsDefault,
|
|
178
|
+
) {
|
|
179
|
+
if (!delta || Object.keys(delta).length === 0) return false;
|
|
180
|
+
const merged = mergeStats(loadHookStats(file, fs), delta);
|
|
181
|
+
return saveHookStats(merged, file, fs);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
hookKey,
|
|
186
|
+
defaultHookStatsPath,
|
|
187
|
+
emptyStats,
|
|
188
|
+
aggregateRun,
|
|
189
|
+
mergeStats,
|
|
190
|
+
boundStats,
|
|
191
|
+
toCheckHooksInput,
|
|
192
|
+
loadHookStats,
|
|
193
|
+
saveHookStats,
|
|
194
|
+
persistSessionStats,
|
|
195
|
+
MAX_HOOKS,
|
|
196
|
+
DEFAULT_CIRCUIT_THRESHOLD,
|
|
197
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-addressed idempotency — the P0-2 "Diff Apply 使用内容哈希/版本号" +
|
|
3
|
+
* "外部 API 优先使用 Provider Idempotency Key" primitives.
|
|
4
|
+
*
|
|
5
|
+
* All PURE (deterministic, no clock / RNG / I/O):
|
|
6
|
+
*
|
|
7
|
+
* - `classifyEditReplay` — decide, from the CURRENT file content alone, whether
|
|
8
|
+
* a literal edit still applies, has ALREADY landed (a resumed replay), or
|
|
9
|
+
* genuinely conflicts. Lets a recovered worker skip an edit it already made
|
|
10
|
+
* instead of double-applying it or erroring confusingly.
|
|
11
|
+
* - `editIdempotencyKey` / `operationIdempotencyKey` — stable keys derived from
|
|
12
|
+
* the operation and a canonicalized (order-independent) view of its args, so
|
|
13
|
+
* a retry of the SAME effect yields the SAME key. An external provider that
|
|
14
|
+
* honors `Idempotency-Key` then de-dupes a resumed replay; internally the key
|
|
15
|
+
* tags each side-effect ledger op for duplicate detection.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
|
|
20
|
+
function sha256Hex(input) {
|
|
21
|
+
return createHash("sha256").update(String(input), "utf8").digest("hex");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Canonicalize a value so semantically-equal args hash identically regardless of
|
|
26
|
+
* key order. Arrays keep order (order is meaningful); object keys are sorted.
|
|
27
|
+
*/
|
|
28
|
+
function canonicalize(v) {
|
|
29
|
+
if (v == null) return null;
|
|
30
|
+
if (Array.isArray(v)) return v.map(canonicalize);
|
|
31
|
+
if (typeof v === "object") {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const k of Object.keys(v).sort()) out[k] = canonicalize(v[k]);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Fate of an edit checked against the current on-disk content. */
|
|
40
|
+
export const EDIT_REPLAY = Object.freeze({
|
|
41
|
+
APPLY: "apply", // old_string present — the edit still applies
|
|
42
|
+
ALREADY_APPLIED: "already_applied", // old gone, new present — idempotent no-op
|
|
43
|
+
CONFLICT: "conflict", // neither present — a genuine mismatch, surface it
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decide the fate of a literal (old→new) edit against CURRENT content — the
|
|
48
|
+
* content-addressed idempotency check. On a resumed replay the edit already
|
|
49
|
+
* landed, so `old_string` is gone and `new_string` is present → already_applied
|
|
50
|
+
* (skip, never double-apply). PURE.
|
|
51
|
+
*
|
|
52
|
+
* Deletions (`new_string === ""`) are deliberately NOT auto-detected as
|
|
53
|
+
* already-applied: "old absent + nothing to look for" is indistinguishable from
|
|
54
|
+
* "old never existed", so those keep surfacing as a conflict (the safe default).
|
|
55
|
+
*
|
|
56
|
+
* @param {{content:string, oldString:string, newString:string, replaceAll?:boolean}} p
|
|
57
|
+
* @returns {"apply"|"already_applied"|"conflict"}
|
|
58
|
+
*/
|
|
59
|
+
export function classifyEditReplay({ content, oldString, newString }) {
|
|
60
|
+
const text = String(content ?? "");
|
|
61
|
+
const oldS = String(oldString ?? "");
|
|
62
|
+
const newS = String(newString ?? "");
|
|
63
|
+
if (oldS && text.includes(oldS)) return EDIT_REPLAY.APPLY;
|
|
64
|
+
// old_string is absent. A present, non-empty new_string means the intended
|
|
65
|
+
// change is already in the file — the edit already happened.
|
|
66
|
+
if (newS && text.includes(newS)) return EDIT_REPLAY.ALREADY_APPLIED;
|
|
67
|
+
return EDIT_REPLAY.CONFLICT;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Stable idempotency key for a literal edit against a target file. */
|
|
71
|
+
export function editIdempotencyKey({ path, oldString, newString, replaceAll }) {
|
|
72
|
+
return (
|
|
73
|
+
"edit_" +
|
|
74
|
+
sha256Hex(
|
|
75
|
+
JSON.stringify([
|
|
76
|
+
String(path || ""),
|
|
77
|
+
String(oldString ?? ""),
|
|
78
|
+
String(newString ?? ""),
|
|
79
|
+
replaceAll === true,
|
|
80
|
+
]),
|
|
81
|
+
).slice(0, 40)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Deterministic idempotency key for an external, irreversible operation — stable
|
|
87
|
+
* across retries so a provider honoring `Idempotency-Key` de-dupes a resumed
|
|
88
|
+
* replay, and so the side-effect ledger can spot a duplicated commit. Derived
|
|
89
|
+
* from the tool + a canonicalized view of its args; NO wall-clock / RNG so the
|
|
90
|
+
* SAME logical effect always maps to the SAME key.
|
|
91
|
+
*
|
|
92
|
+
* `scope` is optional namespacing (e.g. a session id); OMIT it when you want a
|
|
93
|
+
* pure retry of the same call to collide with its earlier attempt.
|
|
94
|
+
*
|
|
95
|
+
* @param {{tool:string, args?:object, scope?:string}} p
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function operationIdempotencyKey({ tool, args, scope } = {}) {
|
|
99
|
+
return (
|
|
100
|
+
"op_" +
|
|
101
|
+
sha256Hex(
|
|
102
|
+
JSON.stringify([
|
|
103
|
+
String(tool || ""),
|
|
104
|
+
scope == null ? "" : String(scope),
|
|
105
|
+
canonicalize(args ?? null),
|
|
106
|
+
]),
|
|
107
|
+
).slice(0, 40)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
LEGACY_TO_UNIFIED_TYPE,
|
|
15
15
|
} from "../runtime/runtime-events.js";
|
|
16
16
|
import { createAbortError } from "./abort-utils.js";
|
|
17
|
+
import { verifyApprovalBinding } from "./agent-authority.js";
|
|
17
18
|
import { createEnvelope } from "@chainlesschain/session-core";
|
|
18
19
|
|
|
19
20
|
// Phase 5: parallel service-envelope emission. Map WS agent-handler event
|
|
@@ -167,12 +168,21 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
167
168
|
reject,
|
|
168
169
|
timeoutId,
|
|
169
170
|
kind: options.kind || null,
|
|
171
|
+
// Approval binding (authority §"权限来源"): when a permission gate raises
|
|
172
|
+
// this request WITH a binding (tool_call_id + args + policy digest), an
|
|
173
|
+
// *approve* answer must echo the SAME binding or it is a stale / mis-
|
|
174
|
+
// routed / argument-tampered verdict and is rejected in _resolvePending.
|
|
175
|
+
// null (the default for plain questions) → no binding check, unchanged.
|
|
176
|
+
binding: options.binding || null,
|
|
170
177
|
});
|
|
171
178
|
|
|
172
179
|
this._sendWs({
|
|
173
180
|
...message,
|
|
174
181
|
sessionId: this.sessionId,
|
|
175
182
|
requestId,
|
|
183
|
+
// Ride the binding out so the remote UI/bridge can echo it back on the
|
|
184
|
+
// answer (only present when the caller supplied one).
|
|
185
|
+
...(options.binding ? { binding: options.binding } : {}),
|
|
176
186
|
});
|
|
177
187
|
});
|
|
178
188
|
}
|
|
@@ -210,8 +220,8 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
210
220
|
* Called by ws-server when a session-answer message arrives.
|
|
211
221
|
* Resolves the corresponding pending promise.
|
|
212
222
|
*/
|
|
213
|
-
resolveAnswer(requestId, answer) {
|
|
214
|
-
this._resolvePending(requestId, answer, "question");
|
|
223
|
+
resolveAnswer(requestId, answer, binding = null) {
|
|
224
|
+
this._resolvePending(requestId, answer, "question", binding);
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
async requestHostTool(toolName, args = {}, extra = {}) {
|
|
@@ -321,7 +331,12 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
321
331
|
}
|
|
322
332
|
}
|
|
323
333
|
|
|
324
|
-
_resolvePending(
|
|
334
|
+
_resolvePending(
|
|
335
|
+
requestId,
|
|
336
|
+
payload,
|
|
337
|
+
expectedKind = null,
|
|
338
|
+
incomingBinding = null,
|
|
339
|
+
) {
|
|
325
340
|
const pending = this._pending.get(requestId);
|
|
326
341
|
if (!pending) {
|
|
327
342
|
return;
|
|
@@ -337,11 +352,26 @@ export class WebSocketInteractionAdapter extends InteractionAdapter {
|
|
|
337
352
|
return;
|
|
338
353
|
}
|
|
339
354
|
|
|
355
|
+
// Approval-binding guard (authority §"权限来源"): if this request was raised
|
|
356
|
+
// with a binding AND the answer echoes a binding that does NOT match, the
|
|
357
|
+
// verdict is for a different / argument-tampered call — settle as a DENY
|
|
358
|
+
// (fail closed) instead of the approve it claims to be, mirroring the local
|
|
359
|
+
// headless approval gate. Backward-compatible: no request binding, or an
|
|
360
|
+
// answer with no echoed binding, resolves exactly as before.
|
|
361
|
+
let effective = payload;
|
|
362
|
+
if (
|
|
363
|
+
pending.binding &&
|
|
364
|
+
incomingBinding &&
|
|
365
|
+
!verifyApprovalBinding(pending.binding, incomingBinding)
|
|
366
|
+
) {
|
|
367
|
+
effective = false;
|
|
368
|
+
}
|
|
369
|
+
|
|
340
370
|
this._pending.delete(requestId);
|
|
341
371
|
if (pending.timeoutId) {
|
|
342
372
|
clearTimeout(pending.timeoutId);
|
|
343
373
|
}
|
|
344
|
-
pending.resolve(
|
|
374
|
+
pending.resolve(effective);
|
|
345
375
|
}
|
|
346
376
|
}
|
|
347
377
|
|
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
* `deps.writeOut` capture seam — the runner itself is untouched: each attempt
|
|
8
8
|
* runs with output captured, the validated JSON is the only thing printed.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Constrained validation delegates to the richer Draft-2020-12-subset validator
|
|
11
|
+
* in [[json-schema-validate.js]] (type, properties, required, items/prefixItems,
|
|
12
|
+
* enum/const, additionalProperties, min/max, pattern, format, allOf/anyOf/oneOf/
|
|
13
|
+
* not, if/then/else, local $ref) so retries get precise coded/pointered errors —
|
|
14
|
+
* the model is told exactly WHICH JSON Pointer failed and WHY. The legacy
|
|
15
|
+
* `validateAgainstSchema` (loose `$.a.b` strings) stays exported for callers.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import fsDefault from "fs";
|
|
16
19
|
import {
|
|
20
|
+
validate,
|
|
17
21
|
validateSchema,
|
|
18
22
|
buildStructuredResultEvent,
|
|
19
23
|
computeSchemaDigest,
|
|
@@ -96,6 +100,20 @@ export function validateAgainstSchema(value, schema, path = "$") {
|
|
|
96
100
|
return errors;
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Render the richer validator's coded/pointered error objects as human-readable
|
|
105
|
+
* `<JSON Pointer>: <message>` strings for the corrective retry prompt and the
|
|
106
|
+
* final failure report. The empty root pointer is shown as `(root)`.
|
|
107
|
+
*
|
|
108
|
+
* @param {Array<{instancePath?:string, message:string}>} errors
|
|
109
|
+
* @returns {string[]}
|
|
110
|
+
*/
|
|
111
|
+
export function formatSchemaErrors(errors) {
|
|
112
|
+
return (errors || []).map(
|
|
113
|
+
(e) => `${e.instancePath || "(root)"}: ${e.message}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
/**
|
|
100
118
|
* Return the first *balanced* JSON object/array substring in `text`, honoring
|
|
101
119
|
* string literals so braces inside strings don't throw off the depth count.
|
|
@@ -203,18 +221,29 @@ export function loadSchemaFile(fs, schemaFile) {
|
|
|
203
221
|
"No schema provided: pass --json-schema <file> or a schema object",
|
|
204
222
|
);
|
|
205
223
|
}
|
|
224
|
+
// Inline JSON support: a value that starts with `{` is treated as a literal
|
|
225
|
+
// schema object, never a path (a filesystem path can't start with `{`). This
|
|
226
|
+
// lets `--json-schema '{"type":"object",...}'` work without a temp file.
|
|
227
|
+
const trimmed = String(schemaFile).trim();
|
|
228
|
+
const isInline = trimmed.startsWith("{");
|
|
206
229
|
let raw;
|
|
207
|
-
|
|
208
|
-
raw =
|
|
209
|
-
}
|
|
210
|
-
|
|
230
|
+
if (isInline) {
|
|
231
|
+
raw = trimmed;
|
|
232
|
+
} else {
|
|
233
|
+
try {
|
|
234
|
+
raw = fs.readFileSync(schemaFile, "utf-8");
|
|
235
|
+
} catch (e) {
|
|
236
|
+
throw new Error(`Cannot read schema file "${schemaFile}": ${e.message}`);
|
|
237
|
+
}
|
|
211
238
|
}
|
|
212
239
|
let parsed;
|
|
213
240
|
try {
|
|
214
241
|
parsed = JSON.parse(raw);
|
|
215
242
|
} catch (e) {
|
|
216
243
|
throw new Error(
|
|
217
|
-
|
|
244
|
+
isInline
|
|
245
|
+
? `Invalid inline JSON schema: ${e.message}`
|
|
246
|
+
: `Invalid JSON in schema file "${schemaFile}": ${e.message}`,
|
|
218
247
|
);
|
|
219
248
|
}
|
|
220
249
|
// Startup schema meta-validation (P2): reject a malformed schema up front
|
|
@@ -226,7 +255,9 @@ export function loadSchemaFile(fs, schemaFile) {
|
|
|
226
255
|
.slice(0, 5)
|
|
227
256
|
.map((e) => ` - ${e.schemaPath || "/"}: ${e.message}`)
|
|
228
257
|
.join("\n");
|
|
229
|
-
throw new Error(
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Invalid JSON Schema in ${isInline ? "inline schema" : `"${schemaFile}"`}:\n${detail}`,
|
|
260
|
+
);
|
|
230
261
|
}
|
|
231
262
|
return parsed;
|
|
232
263
|
}
|
|
@@ -275,12 +306,15 @@ export async function runJsonSchemaConstrained(cfg = {}) {
|
|
|
275
306
|
lastRaw = raw;
|
|
276
307
|
const parsed = extractJsonPayload(raw);
|
|
277
308
|
if (parsed.ok) {
|
|
278
|
-
|
|
279
|
-
if
|
|
309
|
+
// Richer validator: enforces the full supported subset (format assertions,
|
|
310
|
+
// if/then/else, min/max, pattern, combinators, …) and yields coded/pointered
|
|
311
|
+
// errors so the retry prompt tells the model exactly which pointer failed.
|
|
312
|
+
const { valid, errors } = validate(parsed.value, schema);
|
|
313
|
+
if (valid) {
|
|
280
314
|
writeOut(`${JSON.stringify(parsed.value, null, 2)}\n`);
|
|
281
315
|
return 0;
|
|
282
316
|
}
|
|
283
|
-
lastErrors = errors;
|
|
317
|
+
lastErrors = formatSchemaErrors(errors);
|
|
284
318
|
} else {
|
|
285
319
|
lastErrors = [parsed.error];
|
|
286
320
|
}
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
* Supported keywords: type (+ integer), enum, const, properties, required,
|
|
23
23
|
* additionalProperties (bool|schema), patternProperties-free, items,
|
|
24
24
|
* prefixItems, minItems/maxItems, uniqueItems, minLength/maxLength, pattern,
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* format (date-time/date/time/email/uri/uuid/ipv4/ipv6/hostname — asserted;
|
|
26
|
+
* unknown formats pass through as annotations), minimum/maximum/
|
|
27
|
+
* exclusiveMinimum/exclusiveMaximum, multipleOf, minProperties/maxProperties,
|
|
28
|
+
* allOf/anyOf/oneOf/not, if/then/else, and local `$ref` (`#/$defs/…`,
|
|
29
|
+
* `#/definitions/…`, or any in-document JSON Pointer).
|
|
28
30
|
*/
|
|
29
31
|
|
|
30
32
|
import crypto from "crypto";
|
|
@@ -41,6 +43,97 @@ const KNOWN_TYPES = new Set([
|
|
|
41
43
|
|
|
42
44
|
const MAX_REF_DEPTH = 128; // guards recursive $ref against cyclic data
|
|
43
45
|
|
|
46
|
+
// ─── format assertions ───────────────────────────────────────────────────────
|
|
47
|
+
// Draft 2020-12 `format` is asserted here (the protocol-facing structured-output
|
|
48
|
+
// contract wants it enforced, not merely annotated). A format not in this map is
|
|
49
|
+
// treated as an annotation and always passes. All checkers are pure strings→bool.
|
|
50
|
+
|
|
51
|
+
function isCalendarDate(y, m, d) {
|
|
52
|
+
if (m < 1 || m > 12 || d < 1) return false;
|
|
53
|
+
const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
|
|
54
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
55
|
+
return d <= days[m - 1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkDate(v) {
|
|
59
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
|
|
60
|
+
return !!m && isCalendarDate(+m[1], +m[2], +m[3]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkTime(v) {
|
|
64
|
+
const m = /^(\d{2}):(\d{2}):(\d{2})(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?$/.exec(v);
|
|
65
|
+
if (!m) return false;
|
|
66
|
+
const hh = +m[1];
|
|
67
|
+
const mm = +m[2];
|
|
68
|
+
const ss = +m[3];
|
|
69
|
+
return hh <= 23 && mm <= 59 && ss <= 60; // 60 allows a leap second
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function checkDateTime(v) {
|
|
73
|
+
const m = /^(.+)[Tt](.+)$/.exec(v);
|
|
74
|
+
if (!m) return false;
|
|
75
|
+
// RFC 3339 date-time requires an offset/Z on the time part.
|
|
76
|
+
if (!/([Zz]|[+-]\d{2}:\d{2})$/.test(m[2])) return false;
|
|
77
|
+
return checkDate(m[1]) && checkTime(m[2]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function checkOctet(s) {
|
|
81
|
+
if (!/^\d{1,3}$/.test(s)) return false;
|
|
82
|
+
if (s.length > 1 && s[0] === "0") return false; // no leading zeros
|
|
83
|
+
return +s <= 255;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkIpv4(v) {
|
|
87
|
+
const parts = v.split(".");
|
|
88
|
+
return parts.length === 4 && parts.every(checkOctet);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checkIpv6(v) {
|
|
92
|
+
if (typeof v !== "string" || v.length === 0) return false;
|
|
93
|
+
// At most one "::" compression; split on it.
|
|
94
|
+
const halves = v.split("::");
|
|
95
|
+
if (halves.length > 2) return false;
|
|
96
|
+
const hextet = /^[0-9a-fA-F]{1,4}$/;
|
|
97
|
+
if (halves.length === 2) {
|
|
98
|
+
const left = halves[0] === "" ? [] : halves[0].split(":");
|
|
99
|
+
const right = halves[1] === "" ? [] : halves[1].split(":");
|
|
100
|
+
if (
|
|
101
|
+
!left.every((g) => hextet.test(g)) ||
|
|
102
|
+
!right.every((g) => hextet.test(g))
|
|
103
|
+
)
|
|
104
|
+
return false;
|
|
105
|
+
return left.length + right.length <= 7; // "::" stands for ≥1 zero group
|
|
106
|
+
}
|
|
107
|
+
const groups = v.split(":");
|
|
108
|
+
return groups.length === 8 && groups.every((g) => hextet.test(g));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function checkHostname(v) {
|
|
112
|
+
if (typeof v !== "string" || v.length === 0 || v.length > 253) return false;
|
|
113
|
+
const labels = v.replace(/\.$/, "").split(".");
|
|
114
|
+
return labels.every((l) =>
|
|
115
|
+
/^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(l),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const FORMAT_CHECKERS = {
|
|
120
|
+
"date-time": checkDateTime,
|
|
121
|
+
date: checkDate,
|
|
122
|
+
time: checkTime,
|
|
123
|
+
email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
|
|
124
|
+
uri: (v) => /^[A-Za-z][A-Za-z0-9+.-]*:\S*$/.test(v),
|
|
125
|
+
uuid: (v) =>
|
|
126
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(
|
|
127
|
+
v,
|
|
128
|
+
),
|
|
129
|
+
ipv4: checkIpv4,
|
|
130
|
+
ipv6: checkIpv6,
|
|
131
|
+
hostname: checkHostname,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/** Known format names (exported for meta-validation / callers). */
|
|
135
|
+
export const KNOWN_FORMATS = new Set(Object.keys(FORMAT_CHECKERS));
|
|
136
|
+
|
|
44
137
|
/** RFC 6901 JSON Pointer from a segment array ("" = document root). */
|
|
45
138
|
export function jsonPointer(segments) {
|
|
46
139
|
if (!segments || segments.length === 0) return "";
|
|
@@ -194,6 +287,32 @@ function _validate(value, schema, inst, sch, root, errors, depth) {
|
|
|
194
287
|
_validateObject(value, schema, inst, sch, root, errors, depth);
|
|
195
288
|
|
|
196
289
|
_validateCombinators(value, schema, inst, sch, root, errors, depth);
|
|
290
|
+
_validateConditional(value, schema, inst, sch, root, errors, depth);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* if/then/else — Draft 2020-12 conditional application. `if` is a *test* whose
|
|
295
|
+
* own errors are never reported; if the value matches `if`, `then` applies,
|
|
296
|
+
* otherwise `else` applies. No `if` keyword ⇒ then/else are inert.
|
|
297
|
+
*/
|
|
298
|
+
function _validateConditional(value, schema, inst, sch, root, errors, depth) {
|
|
299
|
+
if (schema.if === undefined) return;
|
|
300
|
+
const matched = _branchValid(value, schema.if, root, depth);
|
|
301
|
+
if (matched) {
|
|
302
|
+
if (schema.then !== undefined) {
|
|
303
|
+
_validate(
|
|
304
|
+
value,
|
|
305
|
+
schema.then,
|
|
306
|
+
inst,
|
|
307
|
+
[...sch, "then"],
|
|
308
|
+
root,
|
|
309
|
+
errors,
|
|
310
|
+
depth,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
} else if (schema.else !== undefined) {
|
|
314
|
+
_validate(value, schema.else, inst, [...sch, "else"], root, errors, depth);
|
|
315
|
+
}
|
|
197
316
|
}
|
|
198
317
|
|
|
199
318
|
function _validateString(value, schema, inst, sch, errors) {
|
|
@@ -232,6 +351,19 @@ function _validateString(value, schema, inst, sch, errors) {
|
|
|
232
351
|
);
|
|
233
352
|
}
|
|
234
353
|
}
|
|
354
|
+
if (typeof schema.format === "string") {
|
|
355
|
+
const check = FORMAT_CHECKERS[schema.format];
|
|
356
|
+
// Unknown format = annotation only (always passes), per Draft 2020-12.
|
|
357
|
+
if (check && !check(value)) {
|
|
358
|
+
_err(
|
|
359
|
+
errors,
|
|
360
|
+
"format",
|
|
361
|
+
inst,
|
|
362
|
+
[...sch, "format"],
|
|
363
|
+
`string is not a valid ${schema.format}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
235
367
|
}
|
|
236
368
|
|
|
237
369
|
function _validateNumber(value, schema, inst, sch, errors) {
|
|
@@ -650,6 +782,14 @@ function _metaValidate(schema, segs, errors, depth) {
|
|
|
650
782
|
}
|
|
651
783
|
if (schema.not !== undefined)
|
|
652
784
|
_metaValidate(schema.not, [...segs, "not"], errors, depth + 1);
|
|
785
|
+
if (schema.format !== undefined && typeof schema.format !== "string") {
|
|
786
|
+
_metaErr(errors, "format", [...segs, "format"], "format must be a string");
|
|
787
|
+
}
|
|
788
|
+
for (const k of ["if", "then", "else"]) {
|
|
789
|
+
if (schema[k] !== undefined) {
|
|
790
|
+
_metaValidate(schema[k], [...segs, k], errors, depth + 1);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
653
793
|
for (const defsKey of ["$defs", "definitions"]) {
|
|
654
794
|
if (schema[defsKey] !== undefined) {
|
|
655
795
|
if (
|