chainlesschain 0.162.162 → 0.162.163
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 +1 -1
- package/src/assets/web-panel/assets/{AIOps-ClI2ALgv.js → AIOps-BQ0kgfA1.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BDjRcJaK.js → ActionButton-Dr1MDANe.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DikSRsaT.js → Analytics-CZ5vsaXW.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-9mLhMFmz.js → AppLayout-BIBga881.js} +3 -3
- package/src/assets/web-panel/assets/{Artifacts-BSr88BrH.js → Artifacts-vpXHRU3u.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CdvyimOF.js → Audit-B8exuoub.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-D3_hCedh.js → BackgroundAgents-923evRRN.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-Cyvv9RHf.js → Backup-sr_sswlQ.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BZM14kt8.js → BaseInput-CnW-4XKT.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DrQSokVz.js → Chat-FZCsTZIL.js} +6 -6
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-bFLYeWMV.js → Checkbox-DFrxB59H.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-7mN7xJXT.js → Codegen-_b6WEel8.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DAB5ZWm7.js → Col-CkbnnZ-K.js} +1 -1
- package/src/assets/web-panel/assets/{Community-B_1TymaK.js → Community-BxM_82zW.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Bq1uSWLJ.js → Compact-DYinP45R.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CyiE1jEe.js → Compliance-DyXp3sSP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Cpk0D7vm.js → Cowork-BEr2DhW7.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-d_Lv3rdB.js → Cron-B-Vwo1hN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DMqhHjMR.js → Crosschain-DbLxKddd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Br-oPuBw.js → DID-BsQ_LFw9.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CfLW_RaP.js → Dashboard-lVlXQHu5.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DZnTOExV.js → Dropdown-Beo_foO7.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DHIUHDv1.js → EmailListRenderer-DszHIXED.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bw3t3myu.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D9cpQPAY.js → Federation-CuJLa2eb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Jdczr9Mr.js → FormItemContext-Cxd1lxhc.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-BT8ZMpC5.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DjARip2b.js → Git-WqPKetx6.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BgWbSRcj.js → Governance-CWYtY3ke.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CfouJciM.js → Inference-BnX1iiNV.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-50l7mrAr.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-rq50H84B.js → Logs-DqLMNntm.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-DUPIEdQH.js → MarkdownRenderer-BqYwEYou.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-BYgPDmHU.js → Marketplace-_vGfmhNl.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-5DVgcFdx.js → McpTools-BeJDOkDj.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-B70LPk7f.js → Memory-Da0EEZDO.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-De8ssA5Y.js → MobileBridge-DSuzcCCy.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-RJsOTsIA.js → Mtc-BE9YgdA0.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-Crh-6BEK.js → MtcAudit-fC2BpO5S.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-wuoR5l3x.js → Multisig-D24Xxqw3.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-U2Mji4fZ.js → NLProgramming-Bs3oGvS8.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CdTZAyDH.js → Notes-BFjmAA-3.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C-FucWE7.js → NotificationSettings-BX_9Bhv0.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DCYFTeaI.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-an3OF85L.js → Organization-DBcdsVEP.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-C-1vmYFe.js → Overflow-DDyaF9vC.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Cwh47xKg.js → P2P-BB78L4he.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Dm38GT0k.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DS2Y8awD.js → Permissions-INbnWv4j.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BQNziA0I.js → PersonalDataHub-BST-VmTq.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-DF85L1ue.js → Pipeline-CE7_tS2k.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BuLornF-.js → Privacy-hK5L8dOB.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C3Y31IwX.js → ProjectInit-BvkkUoDn.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BCPiI8aD.js → ProjectSettings-u7_EUtkm.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BZ6s4k6g.js → Projects-5Fmt_8Nw.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D3Wp022X.js → Providers-B9eanarR.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CUJfQkH4.js → QrScannerModal-C0dX5N2f.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-UAc0EO8l.js → QuickAsk-CKTpG-PZ.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BaxKupcs.js → Recommend-Px_w_XQM.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C75NNnXZ.js → RemoteSession-Di0fnYkM.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BVXynZAk.js → Reputation-vesnWjXt.js} +1 -1
- package/src/assets/web-panel/assets/{Row-C5MRZ0bi.js → Row-exbY6LkT.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DAKkEYka.js → RssFeed-B3O3x70t.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DvWv9YOA.js → Search-uRA0DhNz.js} +1 -1
- package/src/assets/web-panel/assets/{Security-B5peafW1.js → Security-CViqdH6K.js} +4 -4
- package/src/assets/web-panel/assets/{Services-UQmvPEaG.js → Services-CkXS97WN.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-C3bhWpuF.js → Skeleton-DxW5WYnE.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DfkKf9_H.js → Skills-CLxmGFse.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DqMSjlBM.js → Sla-Cr5fziXq.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-C-WbxWUX.js → SpeechSettings-H0JSb3cp.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CYuHZ4N2.js → SyncSettings-DMYWMae7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CyeAyB9d.js → Tasks-CbYyDdF4.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BTCeQnpq.js → Templates-Cpc8JjiK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BifQuA9Y.js → Tenant-CRh77Jda.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-Bx_9Bocf.js → Terminal-4D8tNBO5.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BX0RFCOm.js → TimelineRenderer-D41jzzbG.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CGHrCmUS.js → Tokens-C_jWEy6b.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DlqaY4SO.js → Trigger-Dk1ZUhsy.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DfOmj-ay.js → Trust-CLVy4l7m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-B72jH5fE.js → UkeySign-C-FeqFDj.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BT-63fqv.js → VideoEditing-CqVZzyMm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-Dsul3sHr.js → Wallet-DX9BW9pu.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Izkwwdgy.js → WebAuthn-7hQDpWw_.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-D3FHSiH1.js → WorkflowEditor-6XqUAO0j.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CmVz5z3U.js → chat-5R_vkaJG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CuLgyJBp.js → colors-huUPZdC-.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Ml1YkXpw.js → compact-item-CfJlgisE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-BvjJ48bs.js → createContext-jO8heofr.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DVC0157C.js → hasIn-DY8Vxqx7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJyN28Et.js → index-A48PtXw9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CudmwNBV.js → index-B300RPnT.js} +3 -3
- package/src/assets/web-panel/assets/{index-BbepULSc.js → index-B74OXFxC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DYXHFr4r.js → index-BAON7TzU.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzG-x9p4.js → index-BCqxyvqp.js} +1 -1
- package/src/assets/web-panel/assets/{index-wvb_FtaV.js → index-BHE8OIls.js} +1 -1
- package/src/assets/web-panel/assets/{index-QXzlT9KD.js → index-BQS-uPOK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DukDVXCv.js → index-BRXOP09X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFYVQMvu.js → index-BYLBPpt8.js} +1 -1
- package/src/assets/web-panel/assets/{index-BydjdZnl.js → index-BfCx4pFT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmFNvNLZ.js → index-BliaT02i.js} +1 -1
- package/src/assets/web-panel/assets/{index-KMqhcF7l.js → index-BmqGerpC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ck_52dz_.js → index-BsBtRWbA.js} +1 -1
- package/src/assets/web-panel/assets/{index-C229ABfy.js → index-BtEXSoPL.js} +1 -1
- package/src/assets/web-panel/assets/index-BxUKMRpP.js +1 -0
- package/src/assets/web-panel/assets/{index-C_H_S-DY.js → index-Bxax9vQ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0S8NzO6.js → index-C2iVSFpy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjHBpRYZ.js → index-CA6sO5VD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bzr0rB5T.js → index-CA90TZxG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDGaOWBV.js → index-CLhBh-qE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dt5KMjwJ.js → index-CRElRhyS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BbPtzB8M.js → index-Cbe9G0V4.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_vRgTl9.js → index-ChvHubX3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpFJHiTv.js → index-Cis_Ip7H.js} +1 -1
- package/src/assets/web-panel/assets/{index-KjgKdZe-.js → index-Cjxbu68P.js} +1 -1
- package/src/assets/web-panel/assets/{index-vG4XSJ1d.js → index-CzKmteNS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUt8kTIB.js → index-D0pet5Uy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfuKP4qe.js → index-D1VLuww0.js} +1 -1
- package/src/assets/web-panel/assets/{index-4SBta3d7.js → index-DXBcvUnf.js} +1 -1
- package/src/assets/web-panel/assets/{index-DC_OI9H2.js → index-Db1cLWir.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuGszaWY.js → index-DoI2sZm7.js} +1 -1
- package/src/assets/web-panel/assets/index-DscU9iPI.js +1 -0
- package/src/assets/web-panel/assets/{index-4wcgvPoF.js → index-KRHroryI.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3lD5qap.js → index-d1xkbtNz.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_EvG_GW.js → index-dKx57io7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXkJH-lK.js → index-j3O2EEJe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiC6dQZG.js → index-p85YaaKw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cw0Rjy87.js → index-v3pTaEQm.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_XuRgGn.js → index-vXs3B8GJ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BDgd2YZ8.js → initDefaultProps-MCB7-raZ.js} +1 -1
- package/src/assets/web-panel/assets/{motion-ErtrXdYe.js → motion-UWDF9swV.js} +1 -1
- package/src/assets/web-panel/assets/{move-_GPPNCgs.js → move-CsTmgXZU.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DRLL-8V2.js → mtc-parser-fQy6KGLg.js} +1 -1
- package/src/assets/web-panel/assets/{omit-p86sl023.js → omit-woPnEjd1.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BTu7cMLH.js → pickAttrs-Bs2dZ1gf.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BEKz6Isr.js → placementArrow-C7EVgPkg.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Ck42c9MP.js → responsiveObserve-BH2kYCTN.js} +1 -1
- package/src/assets/web-panel/assets/{slide-1CEvuPsB.js → slide-BFBP2V_e.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BDfdx1GJ.js → statusUtils-xdDxDYse.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-B4K46m5w.js → styleChecker-aAWgPPQf.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DoIVpJNq.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DdAWi-ln.js → useFs-BrOih40N.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-iUu6k8dx.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-DZz4R6Tw.js → vnode-B8lutNSE.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DXlf9sA3.js → zoom-CffNre7V.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +254 -23
- package/src/commands/agent.js +105 -11
- package/src/commands/context.js +125 -14
- package/src/commands/hook.js +181 -7
- package/src/commands/plugin.js +239 -1
- package/src/commands/review.js +347 -2
- package/src/data/changelog.json +9 -4
- package/src/gateways/ws/remote-session-protocol.js +39 -0
- package/src/gateways/ws/session-protocol.js +5 -2
- package/src/lib/agent-schedule-store.js +289 -10
- 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/doctor-checkup.js +52 -0
- package/src/lib/hook-event-log.cjs +193 -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/plugin-runtime/capability-consent.js +175 -0
- package/src/lib/plugin-runtime/policy.js +62 -0
- package/src/lib/plugin-runtime/scopes.js +20 -4
- package/src/lib/project-instructions.js +128 -0
- package/src/lib/repl-goal.js +132 -0
- package/src/lib/repl-rewind.js +52 -0
- package/src/lib/schedule-planner.js +17 -1
- 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 +296 -0
- package/src/lib/sub-agent-context.js +74 -5
- package/src/lib/subagent-inheritance.js +112 -0
- package/src/lib/turn-binding-store.js +119 -0
- package/src/repl/agent-repl.js +136 -0
- package/src/repl/permission-tier.js +28 -0
- package/src/runtime/agent-core.js +376 -10
- package/src/runtime/coding-agent-contract-shared.cjs +60 -1
- package/src/runtime/headless-runner.js +267 -0
- package/src/runtime/headless-stream.js +60 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-HwJR-C6B.js +0 -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,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hook-event-log — a persistent, hash-chained, replay-safe log of the hook
|
|
5
|
+
* delivery envelopes produced by the unified event bus ([[hook-event-bus.cjs]]).
|
|
6
|
+
*
|
|
7
|
+
* The event bus already stamps every settings-hook delivery with a stable
|
|
8
|
+
* `event_id` (see [[settings-hook-events.cjs]] `withDeliveryId`), but until now
|
|
9
|
+
* the full envelope was discarded — so there was nothing to look up when a user
|
|
10
|
+
* wants `cc hook replay <event-id>`. This store persists each envelope to a
|
|
11
|
+
* tamper-evident JSONL log so the replay command has a real source.
|
|
12
|
+
*
|
|
13
|
+
* Design:
|
|
14
|
+
* - OPT-IN: recording only happens when `CC_HOOK_EVENT_LOG` is enabled, so the
|
|
15
|
+
* default hook-firing path is byte-for-byte unchanged (no new IO per turn).
|
|
16
|
+
* - Hash chain: each line is `{envelope, prevHash, hash}` where
|
|
17
|
+
* `hash = sha256(prevHash + stableStringify(envelope))`. Editing / deleting /
|
|
18
|
+
* reordering any recorded line breaks the chain (mirrors the session
|
|
19
|
+
* transcript integrity design), which `verifyHookEventChain` detects.
|
|
20
|
+
* - Size guard: the log rotates to `<file>.1` once it exceeds MAX_LOG_BYTES so
|
|
21
|
+
* an always-on session can't grow it without bound.
|
|
22
|
+
* - Pure over injected IO (`_deps.fs` / `_deps.os`) so it is fully unit-testable
|
|
23
|
+
* without touching the real home directory.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const os = require("os");
|
|
28
|
+
const path = require("path");
|
|
29
|
+
const crypto = require("crypto");
|
|
30
|
+
const { stableStringify } = require("./hook-event-bus.cjs");
|
|
31
|
+
|
|
32
|
+
const _deps = { fs, os };
|
|
33
|
+
|
|
34
|
+
/** Cap the always-on log at 5 MiB, then rotate to `<file>.1`. */
|
|
35
|
+
const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
/** Default log location: `~/.chainlesschain/hook-events.jsonl`. */
|
|
38
|
+
function hookEventLogPath(homedir) {
|
|
39
|
+
const home = homedir || _deps.os.homedir();
|
|
40
|
+
return path.join(home, ".chainlesschain", "hook-events.jsonl");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Recording is opt-in — `CC_HOOK_EVENT_LOG=1` (or `true`) turns it on. */
|
|
44
|
+
function isHookEventLogEnabled(env = process.env) {
|
|
45
|
+
const v = String(env.CC_HOOK_EVENT_LOG || "").toLowerCase();
|
|
46
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Hash one envelope against the previous chain hash (deterministic). */
|
|
50
|
+
function computeChainHash(prevHash, envelope) {
|
|
51
|
+
return crypto
|
|
52
|
+
.createHash("sha256")
|
|
53
|
+
.update(`${prevHash || "genesis"}\n${stableStringify(envelope)}`, "utf-8")
|
|
54
|
+
.digest("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The hash the NEXT record must chain from (last parseable line's `hash`). */
|
|
58
|
+
function latestChainHash(text) {
|
|
59
|
+
const lines = String(text || "").split("\n");
|
|
60
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
61
|
+
if (!lines[i].trim()) continue;
|
|
62
|
+
try {
|
|
63
|
+
const rec = JSON.parse(lines[i]);
|
|
64
|
+
return typeof rec?.hash === "string" ? rec.hash : null;
|
|
65
|
+
} catch {
|
|
66
|
+
// half-written tail line — chain from the previous intact record
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Append a hook envelope to the chained log. Best-effort: never throws — a
|
|
75
|
+
* failure to record must not break the turn that fired the hook.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} envelope a full envelope from buildHookEnvelope
|
|
78
|
+
* @param {object} [opts] { filePath, homedir }
|
|
79
|
+
* @returns {{ok:boolean, event_id?:string, hash?:string, reason?:string}}
|
|
80
|
+
*/
|
|
81
|
+
function appendHookEvent(envelope, { filePath, homedir } = {}) {
|
|
82
|
+
if (!envelope || typeof envelope !== "object" || !envelope.event_id) {
|
|
83
|
+
return { ok: false, reason: "invalid envelope (no event_id)" };
|
|
84
|
+
}
|
|
85
|
+
const f = _deps.fs;
|
|
86
|
+
const fp = filePath || hookEventLogPath(homedir);
|
|
87
|
+
try {
|
|
88
|
+
const dir = path.dirname(fp);
|
|
89
|
+
if (!f.existsSync(dir)) f.mkdirSync(dir, { recursive: true });
|
|
90
|
+
let prevHash = null;
|
|
91
|
+
if (f.existsSync(fp)) {
|
|
92
|
+
let rotated = false;
|
|
93
|
+
try {
|
|
94
|
+
if (f.statSync(fp).size > MAX_LOG_BYTES) {
|
|
95
|
+
f.renameSync(fp, `${fp}.1`);
|
|
96
|
+
rotated = true;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
/* stat/rename best-effort */
|
|
100
|
+
}
|
|
101
|
+
if (!rotated) prevHash = latestChainHash(f.readFileSync(fp, "utf-8"));
|
|
102
|
+
}
|
|
103
|
+
const hash = computeChainHash(prevHash, envelope);
|
|
104
|
+
const line = `${JSON.stringify({ envelope, prevHash, hash })}\n`;
|
|
105
|
+
f.appendFileSync(fp, line, "utf-8");
|
|
106
|
+
return { ok: true, event_id: envelope.event_id, hash };
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { ok: false, reason: e.message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Read every recorded record (skips malformed / half-written lines). */
|
|
113
|
+
function readHookEventRecords({ filePath, homedir } = {}) {
|
|
114
|
+
const f = _deps.fs;
|
|
115
|
+
const fp = filePath || hookEventLogPath(homedir);
|
|
116
|
+
if (!f.existsSync(fp)) return [];
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const line of f.readFileSync(fp, "utf-8").split("\n")) {
|
|
119
|
+
if (!line.trim()) continue;
|
|
120
|
+
try {
|
|
121
|
+
out.push(JSON.parse(line));
|
|
122
|
+
} catch {
|
|
123
|
+
/* skip malformed / partial line */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Look up the envelope for a delivery id. The LATEST matching record wins (an
|
|
131
|
+
* event_id can recur across processes; the most recent delivery is what a user
|
|
132
|
+
* replaying "the last one" means).
|
|
133
|
+
*/
|
|
134
|
+
function findHookEvent(eventId, { filePath, homedir } = {}) {
|
|
135
|
+
if (!eventId) return null;
|
|
136
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
137
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
138
|
+
if (records[i]?.envelope?.event_id === eventId) return records[i].envelope;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The most recent `limit` envelopes (newest last), for `cc hook events-log`. */
|
|
144
|
+
function listHookEvents({ limit = 20, filePath, homedir } = {}) {
|
|
145
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
146
|
+
const n = Math.max(0, Number(limit) || 0);
|
|
147
|
+
return records.slice(n ? -n : 0).map((r) => r.envelope);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Recompute the chain and report the first break, if any.
|
|
152
|
+
* @returns {{ok:boolean, length:number, brokenAt:number, reason?:string}}
|
|
153
|
+
*/
|
|
154
|
+
function verifyHookEventChain({ filePath, homedir } = {}) {
|
|
155
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
156
|
+
let prev = null;
|
|
157
|
+
for (let i = 0; i < records.length; i++) {
|
|
158
|
+
const { envelope, prevHash, hash } = records[i] || {};
|
|
159
|
+
if ((prevHash ?? null) !== prev) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
length: records.length,
|
|
163
|
+
brokenAt: i,
|
|
164
|
+
reason: "prevHash does not match the previous record's hash",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const expect = computeChainHash(prev, envelope);
|
|
168
|
+
if (expect !== hash) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
length: records.length,
|
|
172
|
+
brokenAt: i,
|
|
173
|
+
reason: "record hash does not match its envelope (tampered)",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
prev = hash;
|
|
177
|
+
}
|
|
178
|
+
return { ok: true, length: records.length, brokenAt: -1 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
MAX_LOG_BYTES,
|
|
183
|
+
hookEventLogPath,
|
|
184
|
+
isHookEventLogEnabled,
|
|
185
|
+
computeChainHash,
|
|
186
|
+
latestChainHash,
|
|
187
|
+
appendHookEvent,
|
|
188
|
+
readHookEventRecords,
|
|
189
|
+
findHookEvent,
|
|
190
|
+
listHookEvents,
|
|
191
|
+
verifyHookEventChain,
|
|
192
|
+
_deps,
|
|
193
|
+
};
|
|
@@ -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 (
|