chainlesschain 0.166.32 → 0.166.36
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 +24 -1
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-DKUOkYex.js → AIOps-Zs-zPwVG.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-B3luFrPA.js → ActionButton-HZlxM6H0.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-jbZ8R2S3.js → Analytics-CXyThRDb.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CFS2vzPS.js → AppLayout-DeyYh_EF.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-sS0Qpy5q.js → Artifacts-BQDtMwkx.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CZ7kfjCG.js → Audit-DNH4UOux.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BpWc5SgP.js → BackgroundAgents-BkKdIxE6.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DpEYnvoy.js → Backup-DYy4aJom.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-kFzUGLnl.js → BaseInput-DjoyGVYm.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BP_8aAfJ.js → Chat-D86ZYgOD.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-yFeZ15D4.js → ChatBubbleRenderer-RxI_YYth.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-HR1HQ06d.js → Checkbox-C7p8UY99.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DxAqDPjT.js → Codegen-DnapqgV5.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BaCLih7J.js → Col-C29J2DBT.js} +1 -1
- package/src/assets/web-panel/assets/{Community-ZX64uW8D.js → Community-Bf3KeLCr.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CLtMoXTt.js → Compact-DA83wB48.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BlyxBDrs.js → Compliance-CXXVgRFG.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CtQZIYPh.js → Cowork-B28Ftuqv.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-BebN-ajI.js → Cron-CO-BcnDf.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-3gRU0-58.js → Crosschain-BmgPMFCs.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DSVTtLnb.js → DID-CYW9WslA.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CvnhOWqc.js → Dashboard-CWWhVWds.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BhdofmCJ.js → Dropdown-CjTuc1-n.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Dhpua9Lo.js → EmailListRenderer-BrROAu4d.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DA1mLqQ4.js → FamilyGuardDashboard-Ci8Lmk8W.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-D6SUyKGb.js → Federation-CKzKVLE_.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BcMiwguB.js → FormItemContext-CC2CK5PQ.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CGEB1WcH.js → GenericCardRenderer-BYa3ElyT.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DGCNNqR_.js → Git-Bmn3ucA4.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-D4HVyFdL.js → Governance-DqEOXitg.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BOoEmvKl.js → Inference-BjfeExba.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-ECLI0C0_.js → KnowledgeGraph-CBTyIFlJ.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-Cf9GjHtX.js → Logs-KZfA_M9Q.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-C3J59f8v.js → MarkdownRenderer-BG7vi3V4.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-DeL4rOAJ.js → Marketplace-BFeK4he0.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Dvn3KpLW.js → McpTools-Brg7uUoK.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CSlLy-aw.js → Memory-Ce4AmyX3.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-yNIe8-74.js → MobileBridge-Dx1VBL_E.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CsS8Lozw.js → MobileProjects-DPyRr2Xl.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Q7iMzSds.js → Mtc-BzRmtDQU.js} +3 -3
- package/src/assets/web-panel/assets/{MtcAudit-CW71IQrK.js → MtcAudit-DvZ5kLvJ.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-pursAeeT.js → Multisig-D8uGUjX1.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DtNo1qqO.js → NLProgramming-igc-TQE5.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B1xUFwUT.js → Notes-CTqDYlek.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BgmQs_J8.js → NotificationSettings-CkcLa2rb.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-DI9t8YZl.js → OrderTableRenderer-BfScYvnH.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-BbrrT6gR.js → Organization-BjKK9SdB.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-BcPi4YUc.js → Overflow-BkcghTPE.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DfRteW76.js → P2P-l9f34Xu8.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-De81lu5Q.js → PdhVaultBrowser-yVXWmwWe.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DLCRaOe3.js → Permissions-DeSYs0VV.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-fuTL6z8u.js → PersonalDataHub-CmKmhy0q.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-D3ztsQDw.js → Pipeline-DBxS4R_r.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-4f-SQ4ie.js → Privacy-CKngdGw3.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CC_ZFjrV.js → ProjectInit-0Pso0jXN.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-C9FWHcne.js → ProjectSettings-BHrqWQ6r.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CR3ETMQQ.js → Projects-B8wIh9PK.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BWcbdgXx.js → Providers-CXFTOHRV.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-BavxpdIK.js → QrScannerModal-DfN0c_BN.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CZ7gupY4.js → QuickAsk-v1Z5hty_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BQNpBYLL.js → Recommend-DOaVmZUi.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-2LU0Vr44.js → RemoteSession--EWcPBY1.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-oYWo7qyp.js → Reputation-DUWFSNvV.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B8Yj-Csx.js → Row-0-bFnXWF.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-CbxKbHJr.js → RssFeed-B_uTfubW.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DGe-u9s0.js → Search-CqxiTLfB.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BwjfMAFI.js → Security-CGoHOz5B.js} +2 -2
- package/src/assets/web-panel/assets/{Services-BB5NQHwQ.js → Services-COhAbS45.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BPSVUive.js → Skeleton-B97crp6v.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DU2D-IYQ.js → Skills-er_XdavV.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DLX0vcyj.js → Sla-RpkWkzOL.js} +1 -1
- package/src/assets/web-panel/assets/SpeechSettings-DDVXmT5y.js +1 -0
- package/src/assets/web-panel/assets/{SyncSettings-QuDslViv.js → SyncSettings-Bbiw6ohw.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-C-jF6Kwk.js → Tasks-CRghr7lo.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-B6tY1zm-.js → Templates-BUQkfydG.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BXx5FAY1.js → Tenant-CfUFOsZu.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-CNk8KI4J.js → Terminal-BGY7gi4M.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Bo2YVc2l.js → TimelineRenderer-B3Len0l7.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-Bs1Oq8t9.js → Tokens-BjeHn2zI.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CtDhiQxG.js → Trigger-ctcZ2mcv.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-riAZiPHp.js → Trust-oncF3TpO.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DZRzYaNL.js → UkeySign-BwKbVYCd.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DTwOdg2x.js → VideoEditing-D1dgL-3U.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-ByuDECxb.js → Wallet-4hXAfjBL.js} +3 -3
- package/src/assets/web-panel/assets/{WebAuthn-CkSKfId0.js → WebAuthn-C8p3cotb.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BmyJJndu.js → WorkflowEditor-BnrjG77E.js} +1 -1
- package/src/assets/web-panel/assets/{chat-s1LiqLff.js → chat-B-YNK7EI.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CMKVIFUx.js → colors-B9HYvK-e.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DQGdG4Y5.js → compact-item-BJr0quBA.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-Csln9jj1.js → createContext-CXfbQEWD.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DNpXMZaQ.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-D1RYDhMF.js → hasIn-B7Q02hdZ.js} +1 -1
- package/src/assets/web-panel/assets/index-2hcdmWpb.js +1 -0
- package/src/assets/web-panel/assets/{index-DmIeFdLq.js → index-4gZEpDAG.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUVbDS_3.js → index-6wWd08X-.js} +1 -1
- package/src/assets/web-panel/assets/{index-CiB2ZOQm.js → index-B6h86qjh.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cew8V1UQ.js → index-BB4KIBso.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIFiRztW.js → index-BClzZrmx.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bqhj13Jf.js → index-BNL9lmWL.js} +1 -1
- package/src/assets/web-panel/assets/{index-D1kWd1be.js → index-BU_adeUp.js} +1 -1
- package/src/assets/web-panel/assets/{index-rHRlGEnX.js → index-Bd3nX6aj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTg3OrkW.js → index-Bf0OxvOh.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1y8O1ZP.js → index-BleXwr2w.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGBHLqy3.js → index-BmCD1_HC.js} +1 -1
- package/src/assets/web-panel/assets/{index-B39ap5Fq.js → index-BmvpVSmM.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6eoC11O.js → index-BoNVnnAq.js} +1 -1
- package/src/assets/web-panel/assets/{index-CUlRsoXi.js → index-BtunVZuS.js} +1 -1
- package/src/assets/web-panel/assets/{index--G_4msyB.js → index-BvRqE3Un.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0vls3Cv.js → index-C3M8M9_U.js} +1 -1
- package/src/assets/web-panel/assets/{index-ugL-2h6d.js → index-C7IZWdoB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BxVtT9q8.js → index-CEujQqdl.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8sTim5H.js → index-COdAugDt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DHic4uvV.js → index-CQ7qYZS4.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3TPgm-r.js → index-CU7cBzr9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFHhLE91.js → index-CXgHZ_HG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DauplA4h.js → index-CcyxVqzn.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2v6xn0e.js → index-D0zuXz4s.js} +1 -1
- package/src/assets/web-panel/assets/{index-CHmRaTdN.js → index-D1GP2MQz.js} +1 -1
- package/src/assets/web-panel/assets/{index-FLnF1aVs.js → index-D5g5_oua.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmAjtQdv.js → index-DMoUa3dF.js} +1 -1
- package/src/assets/web-panel/assets/{index-8mlgjcck.js → index-DPUg6cVc.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJsa5jwk.js → index-D_UneDq3.js} +1 -1
- package/src/assets/web-panel/assets/index-DbQCUG4D.js +1 -0
- package/src/assets/web-panel/assets/{index-BsmEoBNu.js → index-DsV1HA0Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bl5BNEwF.js → index-DutYFyC0.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvrkxTu0.js → index-G7Z_On_n.js} +1 -1
- package/src/assets/web-panel/assets/{index-BiT8auXA.js → index-Gjegl5Ig.js} +1 -1
- package/src/assets/web-panel/assets/{index-jPM2-LMz.js → index-HzCaU25O.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQ86qdvJ.js → index-cq5Q-RHa.js} +19 -19
- package/src/assets/web-panel/assets/{index-CJwH7xWo.js → index-iCKFl_jt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DU23ertt.js → index-kAggAkrB.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DWJU65d6.js → initDefaultProps-DrzGM6fO.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B0cCdvX2.js → motion-SE_xFnGL.js} +1 -1
- package/src/assets/web-panel/assets/{move-CSWGW4YZ.js → move-CrPjKNiG.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-_P9unVrC.js → mtc-parser-CpcyFPK6.js} +1 -1
- package/src/assets/web-panel/assets/{omit-eUFaswv5.js → omit-BV0g7vf4.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Hmk4wpqj.js → pickAttrs-lWpsc-k6.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-COIVVq2l.js → placementArrow-CiewvU4X.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CkXZS7fs.js → responsiveObserve-DbF8pamO.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BGdyZ1Ue.js → slide-Bn82Ip9-.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-rbVNSZAN.js → statusUtils-J76gk_-G.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-CcdFI5tK.js → styleChecker-BL95J74a.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-C9yiJQMh.js → useFlexGapSupport-BUzhdQsv.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-B73ulQmg.js → useFs-B2Fppepb.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DNN-hD_j.js → usePersonalDataHub-DjTASfYE.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BN1KizIr.js → vnode-BwrOgBqP.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-0KE4Sg53.js → zoom-B6MeT_Gw.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/llm.js +4 -3
- package/src/data/changelog.json +52 -0
- package/src/lib/evolution/evolution-artifact-ports.js +40 -3
- package/src/lib/evolution/evolution-file-identity.js +50 -0
- package/src/lib/evolution/evolution-file-witness.js +206 -91
- package/src/lib/evolution/evolution-ledger-file-backend.js +4 -0
- package/src/lib/evolution/evolution-ledger.js +37 -5
- package/src/lib/evolution/evolution-witness-segments.js +330 -0
- package/src/lib/evolution/progressive-canary-external-watchdog.js +6 -4
- package/src/lib/evolution/skill-candidate-registry.js +45 -11
- package/src/lib/evolution/skill-release-registry.js +46 -12
- package/src/lib/llm-connection-probe.js +21 -4
- package/src/lib/remote-read-loop-guard.js +72 -13
- package/src/lib/text-file-search-worker.js +147 -0
- package/src/lib/text-file-search.js +116 -0
- package/src/lib/web-fetch-snapshots.js +211 -0
- package/src/lib/web-fetch.js +192 -18
- package/src/lib/web-search.js +118 -10
- package/src/runtime/agent-core.js +44 -0
- package/src/runtime/coding-agent-contract-shared.cjs +98 -4
- package/src/assets/web-panel/assets/SpeechSettings-CsPt1rpa.js +0 -1
- package/src/assets/web-panel/assets/devWarning-C8YOOF96.js +0 -1
- package/src/assets/web-panel/assets/index-BUXHmygu.js +0 -1
- package/src/assets/web-panel/assets/index-CsNpDb_W.js +0 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ensurePrivateDirectory } from "../secure-fs.js";
|
|
3
|
+
|
|
4
|
+
export const EVOLUTION_SEGMENTED_WITNESS_SCHEMA =
|
|
5
|
+
"chainlesschain.evolution-file-witness-store/v2";
|
|
6
|
+
const SEGMENT_SCHEMA = "chainlesschain.evolution-file-witness-segment/v1";
|
|
7
|
+
export const EVOLUTION_WITNESS_SEGMENT_RECORDS = 256;
|
|
8
|
+
const MAX_RECORDS = 262_144;
|
|
9
|
+
const MAX_SEGMENTS = MAX_RECORDS / EVOLUTION_WITNESS_SEGMENT_RECORDS;
|
|
10
|
+
const MAX_CACHE_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
12
|
+
const HEAD_KEYS = new Set([
|
|
13
|
+
"schema",
|
|
14
|
+
"current",
|
|
15
|
+
"segments",
|
|
16
|
+
"history",
|
|
17
|
+
"discardedAnchors",
|
|
18
|
+
]);
|
|
19
|
+
const SEGMENT_KEYS = new Set(["schema", "history", "discardedAnchors"]);
|
|
20
|
+
|
|
21
|
+
function exact(value, keys, label) {
|
|
22
|
+
if (
|
|
23
|
+
!value ||
|
|
24
|
+
typeof value !== "object" ||
|
|
25
|
+
Array.isArray(value) ||
|
|
26
|
+
Object.keys(value).length !== keys.size ||
|
|
27
|
+
Object.keys(value).some((key) => !keys.has(key))
|
|
28
|
+
) {
|
|
29
|
+
throw new Error(`${label} fields are invalid`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matchesDiscard(entry, snapshot) {
|
|
34
|
+
return (
|
|
35
|
+
snapshot &&
|
|
36
|
+
(entry.anchorDigest === snapshot.anchorDigest ||
|
|
37
|
+
entry.headDigest === snapshot.headDigest ||
|
|
38
|
+
entry.segmentDigest === snapshot.segmentDigest)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Internal storage adapter. Record signatures and trust policy stay with the witness. */
|
|
43
|
+
export function createEvolutionWitnessSegments({
|
|
44
|
+
filePath,
|
|
45
|
+
fsImpl,
|
|
46
|
+
readBytes,
|
|
47
|
+
writeBytes,
|
|
48
|
+
confirmDurable,
|
|
49
|
+
validateStore,
|
|
50
|
+
legacySchema,
|
|
51
|
+
canonical,
|
|
52
|
+
hash,
|
|
53
|
+
encodeRecord,
|
|
54
|
+
maximumHistoryBytes,
|
|
55
|
+
}) {
|
|
56
|
+
const segmentDirectory = `${filePath}.segments-v2`;
|
|
57
|
+
const secureOptions = { deps: { fs: fsImpl }, failIfUnavailable: true };
|
|
58
|
+
const metadata = new WeakMap();
|
|
59
|
+
const cache = new Map();
|
|
60
|
+
let cacheBytes = 0;
|
|
61
|
+
|
|
62
|
+
const segmentPath = (digest) => {
|
|
63
|
+
if (!DIGEST.test(digest || ""))
|
|
64
|
+
throw new Error("witness segment digest is invalid");
|
|
65
|
+
return path.join(segmentDirectory, `${digest.slice(7)}.json`);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const assertDirectory = () => {
|
|
69
|
+
ensurePrivateDirectory(segmentDirectory, secureOptions);
|
|
70
|
+
const observed = fsImpl.lstatSync(segmentDirectory);
|
|
71
|
+
if (!observed.isDirectory() || observed.isSymbolicLink()) {
|
|
72
|
+
throw new Error("witness segment directory is invalid");
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const validateSegment = (segment, previous, trustEpoch) => {
|
|
77
|
+
exact(segment, SEGMENT_KEYS, "witness segment");
|
|
78
|
+
if (
|
|
79
|
+
segment.schema !== SEGMENT_SCHEMA ||
|
|
80
|
+
!Array.isArray(segment.history) ||
|
|
81
|
+
segment.history.length !== EVOLUTION_WITNESS_SEGMENT_RECORDS
|
|
82
|
+
) {
|
|
83
|
+
throw new Error("witness segment length or schema is invalid");
|
|
84
|
+
}
|
|
85
|
+
validateStore(
|
|
86
|
+
{
|
|
87
|
+
schema: legacySchema,
|
|
88
|
+
current: segment.history.at(-1),
|
|
89
|
+
history: segment.history,
|
|
90
|
+
discardedAnchors: segment.discardedAnchors,
|
|
91
|
+
},
|
|
92
|
+
trustEpoch,
|
|
93
|
+
previous,
|
|
94
|
+
);
|
|
95
|
+
return segment;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const observe = (records, discardedAnchors, observations) => {
|
|
99
|
+
for (const query of observations.discards) {
|
|
100
|
+
if (
|
|
101
|
+
discardedAnchors.some((entry) => matchesDiscard(entry, query.snapshot))
|
|
102
|
+
) {
|
|
103
|
+
query.matched = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const query of observations.records) {
|
|
107
|
+
const firstGeneration = records[0].generation;
|
|
108
|
+
const index = query.checkpoint.generation - firstGeneration;
|
|
109
|
+
if (Number.isSafeInteger(index) && index >= 0 && index < records.length) {
|
|
110
|
+
const record = records[index];
|
|
111
|
+
if (record.witnessDigest === query.checkpoint.witnessDigest)
|
|
112
|
+
query.record = record;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const remember = (digest, segment, trustEpoch) => {
|
|
118
|
+
if (trustEpoch === null) return;
|
|
119
|
+
// A cached decision belongs only to bytes rehashed on this read and a
|
|
120
|
+
// current verifier epoch. Copy through a small encoding so no parser
|
|
121
|
+
// substring can retain a whole source segment.
|
|
122
|
+
const encoded = canonical({
|
|
123
|
+
first: segment.history[0],
|
|
124
|
+
last: segment.history.at(-1),
|
|
125
|
+
discardedAnchors: segment.discardedAnchors,
|
|
126
|
+
});
|
|
127
|
+
const bytes = 6 * encoded.length;
|
|
128
|
+
const existing = cache.get(digest);
|
|
129
|
+
if (existing) cacheBytes -= existing.bytes;
|
|
130
|
+
cache.delete(digest);
|
|
131
|
+
if (cache.size >= MAX_SEGMENTS || cacheBytes + bytes > MAX_CACHE_BYTES)
|
|
132
|
+
return;
|
|
133
|
+
cache.set(digest, { ...JSON.parse(encoded), trustEpoch, bytes });
|
|
134
|
+
cacheBytes += bytes;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const load = (head, trustEpoch, options = {}) => {
|
|
138
|
+
exact(head, HEAD_KEYS, "segmented witness store");
|
|
139
|
+
if (
|
|
140
|
+
head.schema !== EVOLUTION_SEGMENTED_WITNESS_SCHEMA ||
|
|
141
|
+
!Array.isArray(head.segments) ||
|
|
142
|
+
head.segments.length < 1 ||
|
|
143
|
+
head.segments.length >= MAX_SEGMENTS ||
|
|
144
|
+
!Array.isArray(head.history) ||
|
|
145
|
+
head.history.length < 1 ||
|
|
146
|
+
head.history.length > EVOLUTION_WITNESS_SEGMENT_RECORDS ||
|
|
147
|
+
head.segments.length * EVOLUTION_WITNESS_SEGMENT_RECORDS +
|
|
148
|
+
head.history.length >
|
|
149
|
+
MAX_RECORDS
|
|
150
|
+
) {
|
|
151
|
+
throw new Error("segmented witness store capacity or schema is invalid");
|
|
152
|
+
}
|
|
153
|
+
assertDirectory();
|
|
154
|
+
const seen = new Set();
|
|
155
|
+
const observations = {
|
|
156
|
+
discards: (options.discards || [])
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.map((snapshot) => ({ snapshot, matched: false })),
|
|
159
|
+
records: (options.records || []).map((checkpoint) => ({
|
|
160
|
+
checkpoint,
|
|
161
|
+
record: null,
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
164
|
+
let previous = null;
|
|
165
|
+
let storedBytes = options.headBytes;
|
|
166
|
+
if (
|
|
167
|
+
!Number.isSafeInteger(storedBytes) ||
|
|
168
|
+
storedBytes < 1 ||
|
|
169
|
+
storedBytes > maximumHistoryBytes
|
|
170
|
+
) {
|
|
171
|
+
throw new Error("witness history exceeds its configured maximum size");
|
|
172
|
+
}
|
|
173
|
+
for (const digest of head.segments) {
|
|
174
|
+
if (seen.has(digest)) throw new Error("witness segment is repeated");
|
|
175
|
+
seen.add(digest);
|
|
176
|
+
const bytes = readBytes(
|
|
177
|
+
segmentPath(digest),
|
|
178
|
+
maximumHistoryBytes - storedBytes,
|
|
179
|
+
);
|
|
180
|
+
storedBytes += bytes.length;
|
|
181
|
+
if (hash(bytes) !== digest)
|
|
182
|
+
throw new Error("witness segment bytes changed");
|
|
183
|
+
const cached = cache.get(digest);
|
|
184
|
+
const canReuse =
|
|
185
|
+
cached && trustEpoch !== null && cached.trustEpoch === trustEpoch;
|
|
186
|
+
const needsRecord = observations.records.some(
|
|
187
|
+
({ checkpoint }) =>
|
|
188
|
+
canReuse &&
|
|
189
|
+
checkpoint.generation >= cached.first.generation &&
|
|
190
|
+
checkpoint.generation <= cached.last.generation,
|
|
191
|
+
);
|
|
192
|
+
if (canReuse) {
|
|
193
|
+
if (
|
|
194
|
+
(!previous && cached.first.generation !== 0) ||
|
|
195
|
+
(previous &&
|
|
196
|
+
(cached.first.generation !== previous.generation + 1 ||
|
|
197
|
+
cached.first.previousWitnessDigest !== previous.witnessDigest))
|
|
198
|
+
) {
|
|
199
|
+
throw new Error("witness segment history is not contiguous");
|
|
200
|
+
}
|
|
201
|
+
for (const query of observations.discards) {
|
|
202
|
+
if (
|
|
203
|
+
cached.discardedAnchors.some((entry) =>
|
|
204
|
+
matchesDiscard(entry, query.snapshot),
|
|
205
|
+
)
|
|
206
|
+
) {
|
|
207
|
+
query.matched = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (needsRecord) {
|
|
211
|
+
const parsed = JSON.parse(
|
|
212
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
213
|
+
);
|
|
214
|
+
observe(parsed.history, parsed.discardedAnchors, observations);
|
|
215
|
+
}
|
|
216
|
+
previous = cached.last;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const parsed = JSON.parse(
|
|
220
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
221
|
+
);
|
|
222
|
+
const segment = validateSegment(parsed, previous, trustEpoch);
|
|
223
|
+
observe(segment.history, segment.discardedAnchors, observations);
|
|
224
|
+
remember(digest, segment, trustEpoch);
|
|
225
|
+
previous = segment.history.at(-1);
|
|
226
|
+
}
|
|
227
|
+
const store = validateStore(
|
|
228
|
+
{
|
|
229
|
+
schema: legacySchema,
|
|
230
|
+
current: head.current,
|
|
231
|
+
history: head.history,
|
|
232
|
+
discardedAnchors: head.discardedAnchors,
|
|
233
|
+
},
|
|
234
|
+
trustEpoch,
|
|
235
|
+
previous,
|
|
236
|
+
);
|
|
237
|
+
observe(store.history, store.discardedAnchors, observations);
|
|
238
|
+
metadata.set(store, {
|
|
239
|
+
segments: [...head.segments],
|
|
240
|
+
observations,
|
|
241
|
+
tailPreviousDiscardDigest: previous.discardAccumulatorDigest,
|
|
242
|
+
segmentBytes: storedBytes - options.headBytes,
|
|
243
|
+
});
|
|
244
|
+
return store;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const publish = (store) => {
|
|
248
|
+
const existing = metadata.get(store);
|
|
249
|
+
const segments = [...(existing?.segments || [])];
|
|
250
|
+
if (
|
|
251
|
+
segments.length * EVOLUTION_WITNESS_SEGMENT_RECORDS +
|
|
252
|
+
store.history.length >
|
|
253
|
+
MAX_RECORDS
|
|
254
|
+
) {
|
|
255
|
+
throw new Error("witness history exceeds its configured record capacity");
|
|
256
|
+
}
|
|
257
|
+
assertDirectory();
|
|
258
|
+
let history = store.history;
|
|
259
|
+
let discardedAnchors = store.discardedAnchors;
|
|
260
|
+
let segmentBytes = existing?.segmentBytes ?? 0;
|
|
261
|
+
let accumulator =
|
|
262
|
+
existing?.tailPreviousDiscardDigest ??
|
|
263
|
+
history[0].discardAccumulatorDigest;
|
|
264
|
+
while (history.length > EVOLUTION_WITNESS_SEGMENT_RECORDS) {
|
|
265
|
+
const records = history.slice(0, EVOLUTION_WITNESS_SEGMENT_RECORDS);
|
|
266
|
+
let discardCount = 0;
|
|
267
|
+
// The first record's predecessor may belong to the preceding segment.
|
|
268
|
+
// The caller carries the exact predecessor accumulator when loading.
|
|
269
|
+
for (const record of records) {
|
|
270
|
+
if (record.discardAccumulatorDigest !== accumulator) discardCount++;
|
|
271
|
+
accumulator = record.discardAccumulatorDigest;
|
|
272
|
+
}
|
|
273
|
+
const part = {
|
|
274
|
+
schema: SEGMENT_SCHEMA,
|
|
275
|
+
history: records,
|
|
276
|
+
discardedAnchors: discardedAnchors.slice(0, discardCount),
|
|
277
|
+
};
|
|
278
|
+
const bytes = Buffer.from(`${canonical(part)}\n`, "utf8");
|
|
279
|
+
segmentBytes += bytes.length;
|
|
280
|
+
if (segmentBytes > maximumHistoryBytes) {
|
|
281
|
+
throw new Error("witness history exceeds its configured maximum size");
|
|
282
|
+
}
|
|
283
|
+
const digest = hash(bytes);
|
|
284
|
+
const destination = segmentPath(digest);
|
|
285
|
+
if (fsImpl.existsSync(destination)) {
|
|
286
|
+
if (!readBytes(destination).equals(bytes)) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
"existing witness segment differs from its content address",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
// A previous process may have stopped after rename and before fsync of
|
|
292
|
+
// the directory. Reuse requires that durability boundary again.
|
|
293
|
+
confirmDurable(destination, bytes);
|
|
294
|
+
} else {
|
|
295
|
+
// The witness CAS lock serializes publication. Only complete, fsynced
|
|
296
|
+
// bytes are renamed into this immutable content address before HEAD.
|
|
297
|
+
writeBytes(destination, bytes);
|
|
298
|
+
}
|
|
299
|
+
segments.push(digest);
|
|
300
|
+
history = history.slice(EVOLUTION_WITNESS_SEGMENT_RECORDS);
|
|
301
|
+
discardedAnchors = discardedAnchors.slice(discardCount);
|
|
302
|
+
}
|
|
303
|
+
const encoded = `{"current":${encodeRecord(store.current)},"discardedAnchors":${canonical(discardedAnchors)},"history":[${history.map(encodeRecord).join(",")}],"schema":${JSON.stringify(EVOLUTION_SEGMENTED_WITNESS_SCHEMA)},"segments":${canonical(segments)}}`;
|
|
304
|
+
const headBytes = Buffer.from(`${encoded}\n`, "utf8");
|
|
305
|
+
if (segmentBytes + headBytes.length > maximumHistoryBytes) {
|
|
306
|
+
throw new Error("witness history exceeds its configured maximum size");
|
|
307
|
+
}
|
|
308
|
+
writeBytes(filePath, headBytes);
|
|
309
|
+
return store.current;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
load,
|
|
314
|
+
publish,
|
|
315
|
+
handles: (store) =>
|
|
316
|
+
metadata.has(store) ||
|
|
317
|
+
store.history.length > EVOLUTION_WITNESS_SEGMENT_RECORDS,
|
|
318
|
+
isSegmented: (store) => metadata.has(store),
|
|
319
|
+
isDiscarded: (store, snapshot) =>
|
|
320
|
+
metadata
|
|
321
|
+
.get(store)
|
|
322
|
+
?.observations.discards.find((query) => query.snapshot === snapshot)
|
|
323
|
+
?.matched === true,
|
|
324
|
+
lookup: (store, checkpoint) =>
|
|
325
|
+
metadata
|
|
326
|
+
.get(store)
|
|
327
|
+
?.observations.records.find((query) => query.checkpoint === checkpoint)
|
|
328
|
+
?.record ?? null,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -218,6 +218,7 @@ export function createProgressiveCanaryHeartbeatAuthority({
|
|
|
218
218
|
const authority = Object.freeze({
|
|
219
219
|
planDigest: plan.planDigest,
|
|
220
220
|
async issue({ sequence, stage, activeStateDigest } = {}) {
|
|
221
|
+
const issuedAt = timestamp(Number(now()), "heartbeat clock");
|
|
221
222
|
const payload = {
|
|
222
223
|
schema: PROGRESSIVE_CANARY_HOST_HEARTBEAT_SCHEMA,
|
|
223
224
|
planDigest: plan.planDigest,
|
|
@@ -228,15 +229,16 @@ export function createProgressiveCanaryHeartbeatAuthority({
|
|
|
228
229
|
sequence: timestamp(sequence, "heartbeat sequence"),
|
|
229
230
|
stage: id(stage, "heartbeat stage"),
|
|
230
231
|
activeStateDigest: digest(activeStateDigest, "activeStateDigest"),
|
|
231
|
-
issuedAt
|
|
232
|
-
expiresAt:
|
|
232
|
+
issuedAt,
|
|
233
|
+
expiresAt: timestamp(
|
|
234
|
+
issuedAt + plan.leaseDurationMs,
|
|
235
|
+
"heartbeat expiry",
|
|
236
|
+
),
|
|
233
237
|
authorityId: plan.heartbeatAuthority.id,
|
|
234
238
|
authorityRevision: plan.heartbeatAuthority.revision,
|
|
235
239
|
handlerDigest: plan.heartbeatAuthority.handlerDigest,
|
|
236
240
|
publicKeySpkiDigest: plan.heartbeatAuthority.publicKeySpkiDigest,
|
|
237
241
|
};
|
|
238
|
-
if (payload.expiresAt !== payload.issuedAt + plan.leaseDurationMs)
|
|
239
|
-
throw new Error("heartbeat clock changed during issuance");
|
|
240
242
|
const signature = await attestor(Object.freeze(structuredClone(payload)));
|
|
241
243
|
const core = { ...payload, signature };
|
|
242
244
|
return verify(
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { types as utilTypes } from "node:util";
|
|
5
5
|
import { getHomeDir } from "../paths.js";
|
|
6
6
|
import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
|
|
7
|
+
import { withEvolutionFileIdentity } from "./evolution-file-identity.js";
|
|
7
8
|
import {
|
|
8
9
|
verifySkillDependencyLock,
|
|
9
10
|
verifySkillRuntimeManifest,
|
|
@@ -1793,6 +1794,15 @@ export class SkillCandidateRegistry {
|
|
|
1793
1794
|
}
|
|
1794
1795
|
|
|
1795
1796
|
_initializeTenantMarker() {
|
|
1797
|
+
return withEvolutionFileIdentity(
|
|
1798
|
+
this._fs,
|
|
1799
|
+
this._markerPath,
|
|
1800
|
+
(_full, sameStable) =>
|
|
1801
|
+
this.#initializeTenantMarkerWithIdentity(sameStable),
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
#initializeTenantMarkerWithIdentity(sameStable) {
|
|
1796
1806
|
const existing = lstatOrNull(this._fs, this._markerPath);
|
|
1797
1807
|
if (existing) {
|
|
1798
1808
|
const verified = this._readAndVerifyTenantMarker();
|
|
@@ -1832,7 +1842,6 @@ export class SkillCandidateRegistry {
|
|
|
1832
1842
|
"candidate tenant marker temporary file is unsafe",
|
|
1833
1843
|
);
|
|
1834
1844
|
}
|
|
1835
|
-
const writtenIdentity = entryIdentity(written);
|
|
1836
1845
|
this._fs.closeSync(descriptor);
|
|
1837
1846
|
descriptor = null;
|
|
1838
1847
|
if (this._secure) {
|
|
@@ -1847,7 +1856,7 @@ export class SkillCandidateRegistry {
|
|
|
1847
1856
|
staged.isSymbolicLink() ||
|
|
1848
1857
|
Number(staged.nlink) !== 1 ||
|
|
1849
1858
|
staged.size !== bytes.length ||
|
|
1850
|
-
|
|
1859
|
+
!sameStable(staged, written) ||
|
|
1851
1860
|
!samePath(realpath(this._fs, temporaryPath), temporaryPath)
|
|
1852
1861
|
) {
|
|
1853
1862
|
throw registryError(
|
|
@@ -1870,7 +1879,7 @@ export class SkillCandidateRegistry {
|
|
|
1870
1879
|
!linked.isFile() ||
|
|
1871
1880
|
linked.isSymbolicLink() ||
|
|
1872
1881
|
Number(linked.nlink) !== 2 ||
|
|
1873
|
-
entryIdentity(linked) !==
|
|
1882
|
+
entryIdentity(linked) !== entryIdentity(staged)
|
|
1874
1883
|
) {
|
|
1875
1884
|
throw registryError(
|
|
1876
1885
|
"SKILL_CANDIDATE_STORE_UNSAFE",
|
|
@@ -1901,6 +1910,24 @@ export class SkillCandidateRegistry {
|
|
|
1901
1910
|
}
|
|
1902
1911
|
|
|
1903
1912
|
_readBoundedRegularFile(filePath, maximum, code, label) {
|
|
1913
|
+
return withEvolutionFileIdentity(this._fs, filePath, (samePathHandle) =>
|
|
1914
|
+
this.#readBoundedRegularFileWithIdentity(
|
|
1915
|
+
filePath,
|
|
1916
|
+
maximum,
|
|
1917
|
+
code,
|
|
1918
|
+
label,
|
|
1919
|
+
samePathHandle,
|
|
1920
|
+
),
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
#readBoundedRegularFileWithIdentity(
|
|
1925
|
+
filePath,
|
|
1926
|
+
maximum,
|
|
1927
|
+
code,
|
|
1928
|
+
label,
|
|
1929
|
+
samePathHandle,
|
|
1930
|
+
) {
|
|
1904
1931
|
let descriptor = null;
|
|
1905
1932
|
try {
|
|
1906
1933
|
const before = this._fs.lstatSync(filePath);
|
|
@@ -1925,7 +1952,7 @@ export class SkillCandidateRegistry {
|
|
|
1925
1952
|
if (
|
|
1926
1953
|
!opened.isFile() ||
|
|
1927
1954
|
Number(opened.nlink) !== 1 ||
|
|
1928
|
-
|
|
1955
|
+
!samePathHandle(before, opened) ||
|
|
1929
1956
|
opened.size !== before.size
|
|
1930
1957
|
) {
|
|
1931
1958
|
throw registryError(code, `${label} changed while it was opened`);
|
|
@@ -1941,12 +1968,12 @@ export class SkillCandidateRegistry {
|
|
|
1941
1968
|
!afterPath.isFile() ||
|
|
1942
1969
|
afterPath.isSymbolicLink() ||
|
|
1943
1970
|
Number(afterPath.nlink) !== 1 ||
|
|
1944
|
-
|
|
1971
|
+
!samePathHandle(afterPath, after) ||
|
|
1945
1972
|
!samePath(realpath(this._fs, filePath), filePath)
|
|
1946
1973
|
) {
|
|
1947
1974
|
throw registryError(code, `${label} changed while it was read`);
|
|
1948
1975
|
}
|
|
1949
|
-
return { bytes, identity: entryIdentity(
|
|
1976
|
+
return { bytes, identity: entryIdentity(before) };
|
|
1950
1977
|
} finally {
|
|
1951
1978
|
if (descriptor !== null) this._fs.closeSync(descriptor);
|
|
1952
1979
|
}
|
|
@@ -2637,6 +2664,14 @@ export class SkillCandidateRegistry {
|
|
|
2637
2664
|
"candidate create accepts only input; admission context is registry-owned",
|
|
2638
2665
|
);
|
|
2639
2666
|
}
|
|
2667
|
+
return withEvolutionFileIdentity(
|
|
2668
|
+
this._fs,
|
|
2669
|
+
this._markerPath,
|
|
2670
|
+
(_full, sameStable) => this.#createWithIdentity(input, sameStable),
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
#createWithIdentity(input, sameStable) {
|
|
2640
2675
|
this._assertNoMixedTenantArtifacts();
|
|
2641
2676
|
const verificationContext = this._resolveAdmissionContext(input);
|
|
2642
2677
|
const candidate = buildSkillCandidateDraft(input, verificationContext);
|
|
@@ -2684,7 +2719,6 @@ export class SkillCandidateRegistry {
|
|
|
2684
2719
|
{ candidateId: candidate.candidateId, commitState: "not-committed" },
|
|
2685
2720
|
);
|
|
2686
2721
|
}
|
|
2687
|
-
const writtenIdentity = entryIdentity(written);
|
|
2688
2722
|
this._fs.closeSync(descriptor);
|
|
2689
2723
|
descriptor = null;
|
|
2690
2724
|
if (this._secure) {
|
|
@@ -2699,7 +2733,7 @@ export class SkillCandidateRegistry {
|
|
|
2699
2733
|
staged.isSymbolicLink() ||
|
|
2700
2734
|
Number(staged.nlink) !== 1 ||
|
|
2701
2735
|
staged.size !== bytes.length ||
|
|
2702
|
-
|
|
2736
|
+
!sameStable(staged, written) ||
|
|
2703
2737
|
!samePath(realpath(this._fs, temporaryPath), temporaryPath)
|
|
2704
2738
|
) {
|
|
2705
2739
|
throw registryError(
|
|
@@ -2742,7 +2776,7 @@ export class SkillCandidateRegistry {
|
|
|
2742
2776
|
linked.isSymbolicLink() ||
|
|
2743
2777
|
Number(linked.nlink) !== 2 ||
|
|
2744
2778
|
linked.size !== bytes.length ||
|
|
2745
|
-
entryIdentity(linked) !==
|
|
2779
|
+
entryIdentity(linked) !== entryIdentity(staged) ||
|
|
2746
2780
|
!samePath(realpath(this._fs, filePath), filePath)
|
|
2747
2781
|
) {
|
|
2748
2782
|
throw registryError(
|
|
@@ -2759,7 +2793,7 @@ export class SkillCandidateRegistry {
|
|
|
2759
2793
|
finalized.isSymbolicLink() ||
|
|
2760
2794
|
Number(finalized.nlink) !== 1 ||
|
|
2761
2795
|
finalized.size !== bytes.length ||
|
|
2762
|
-
entryIdentity(finalized) !==
|
|
2796
|
+
entryIdentity(finalized) !== entryIdentity(staged)
|
|
2763
2797
|
) {
|
|
2764
2798
|
throw registryError(
|
|
2765
2799
|
"SKILL_CANDIDATE_COMMIT_UNKNOWN",
|
|
@@ -2794,7 +2828,7 @@ export class SkillCandidateRegistry {
|
|
|
2794
2828
|
);
|
|
2795
2829
|
}
|
|
2796
2830
|
if (
|
|
2797
|
-
stored.identity !==
|
|
2831
|
+
stored.identity !== entryIdentity(staged) ||
|
|
2798
2832
|
verified.tenantId !== this.tenantId ||
|
|
2799
2833
|
verified.candidateId !== candidate.candidateId ||
|
|
2800
2834
|
!stored.bytes.equals(bytes) ||
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { types as utilTypes } from "node:util";
|
|
6
6
|
import { getHomeDir } from "../paths.js";
|
|
7
7
|
import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
|
|
8
|
+
import { withEvolutionFileIdentity } from "./evolution-file-identity.js";
|
|
8
9
|
import {
|
|
9
10
|
SKILL_CANDIDATE_MIGRATION_RECEIPT_SCHEMA,
|
|
10
11
|
SKILL_CANDIDATE_MIGRATION_RECORD_SCHEMA,
|
|
@@ -2543,6 +2544,26 @@ function lstatOrNull(fsImpl, target) {
|
|
|
2543
2544
|
}
|
|
2544
2545
|
|
|
2545
2546
|
function readBoundedSingleLinkFile(fsImpl, filePath, maximum, code, label) {
|
|
2547
|
+
return withEvolutionFileIdentity(fsImpl, filePath, (samePathHandle) =>
|
|
2548
|
+
readBoundedSingleLinkFileWithIdentity(
|
|
2549
|
+
fsImpl,
|
|
2550
|
+
filePath,
|
|
2551
|
+
maximum,
|
|
2552
|
+
code,
|
|
2553
|
+
label,
|
|
2554
|
+
samePathHandle,
|
|
2555
|
+
),
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
function readBoundedSingleLinkFileWithIdentity(
|
|
2560
|
+
fsImpl,
|
|
2561
|
+
filePath,
|
|
2562
|
+
maximum,
|
|
2563
|
+
code,
|
|
2564
|
+
label,
|
|
2565
|
+
samePathHandle,
|
|
2566
|
+
) {
|
|
2546
2567
|
let descriptor = null;
|
|
2547
2568
|
try {
|
|
2548
2569
|
const before = fsImpl.lstatSync(filePath);
|
|
@@ -2564,7 +2585,7 @@ function readBoundedSingleLinkFile(fsImpl, filePath, maximum, code, label) {
|
|
|
2564
2585
|
if (
|
|
2565
2586
|
!opened.isFile() ||
|
|
2566
2587
|
Number(opened.nlink) !== 1 ||
|
|
2567
|
-
|
|
2588
|
+
!samePathHandle(before, opened) ||
|
|
2568
2589
|
opened.size !== before.size
|
|
2569
2590
|
) {
|
|
2570
2591
|
throw failure(code, `${label} changed while opening`);
|
|
@@ -2580,12 +2601,12 @@ function readBoundedSingleLinkFile(fsImpl, filePath, maximum, code, label) {
|
|
|
2580
2601
|
!afterPath.isFile() ||
|
|
2581
2602
|
afterPath.isSymbolicLink() ||
|
|
2582
2603
|
Number(afterPath.nlink) !== 1 ||
|
|
2583
|
-
|
|
2604
|
+
!samePathHandle(afterPath, after) ||
|
|
2584
2605
|
!samePath(realpath(fsImpl, filePath), filePath)
|
|
2585
2606
|
) {
|
|
2586
2607
|
throw failure(code, `${label} changed while reading`);
|
|
2587
2608
|
}
|
|
2588
|
-
return { bytes, identity: identity(
|
|
2609
|
+
return { bytes, identity: identity(before) };
|
|
2589
2610
|
} finally {
|
|
2590
2611
|
if (descriptor !== null) fsImpl.closeSync(descriptor);
|
|
2591
2612
|
}
|
|
@@ -2786,8 +2807,7 @@ export class SkillReleaseRegistry {
|
|
|
2786
2807
|
readActive: SkillReleaseRegistry.prototype.readActive.bind(this),
|
|
2787
2808
|
readState: SkillReleaseRegistry.prototype.readState.bind(this),
|
|
2788
2809
|
readRelease: SkillReleaseRegistry.prototype.readRelease.bind(this),
|
|
2789
|
-
readInventory:
|
|
2790
|
-
SkillReleaseRegistry.prototype.readInventory.bind(this),
|
|
2810
|
+
readInventory: SkillReleaseRegistry.prototype.readInventory.bind(this),
|
|
2791
2811
|
matchesTransactionLedger: (value) => value === transactionLedger,
|
|
2792
2812
|
}),
|
|
2793
2813
|
);
|
|
@@ -2870,6 +2890,15 @@ export class SkillReleaseRegistry {
|
|
|
2870
2890
|
}
|
|
2871
2891
|
|
|
2872
2892
|
#initializeTenantMarker() {
|
|
2893
|
+
return withEvolutionFileIdentity(
|
|
2894
|
+
this.#fs,
|
|
2895
|
+
this.#markerPath,
|
|
2896
|
+
(_full, sameStable) =>
|
|
2897
|
+
this.#initializeTenantMarkerWithIdentity(sameStable),
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
#initializeTenantMarkerWithIdentity(sameStable) {
|
|
2873
2902
|
const existing = lstatOrNull(this.#fs, this.#markerPath);
|
|
2874
2903
|
if (existing) {
|
|
2875
2904
|
const verified = this.#readAndVerifyTenantMarker();
|
|
@@ -2905,7 +2934,6 @@ export class SkillReleaseRegistry {
|
|
|
2905
2934
|
"release tenant marker temporary file is unsafe",
|
|
2906
2935
|
);
|
|
2907
2936
|
}
|
|
2908
|
-
const writtenIdentity = identity(written);
|
|
2909
2937
|
this.#fs.closeSync(descriptor);
|
|
2910
2938
|
descriptor = null;
|
|
2911
2939
|
if (this.#secure) {
|
|
@@ -2920,7 +2948,7 @@ export class SkillReleaseRegistry {
|
|
|
2920
2948
|
staged.isSymbolicLink() ||
|
|
2921
2949
|
Number(staged.nlink) !== 1 ||
|
|
2922
2950
|
staged.size !== bytes.length ||
|
|
2923
|
-
|
|
2951
|
+
!sameStable(staged, written) ||
|
|
2924
2952
|
!samePath(realpath(this.#fs, temporaryPath), temporaryPath)
|
|
2925
2953
|
) {
|
|
2926
2954
|
throw failure(
|
|
@@ -2943,7 +2971,7 @@ export class SkillReleaseRegistry {
|
|
|
2943
2971
|
!linked.isFile() ||
|
|
2944
2972
|
linked.isSymbolicLink() ||
|
|
2945
2973
|
Number(linked.nlink) !== 2 ||
|
|
2946
|
-
identity(linked) !==
|
|
2974
|
+
identity(linked) !== identity(staged)
|
|
2947
2975
|
) {
|
|
2948
2976
|
throw failure(
|
|
2949
2977
|
"SKILL_RELEASE_STORE_UNSAFE",
|
|
@@ -3229,6 +3257,12 @@ export class SkillReleaseRegistry {
|
|
|
3229
3257
|
throw failure("SKILL_RELEASE_STORE_UNSAFE", "temporary name is invalid");
|
|
3230
3258
|
}
|
|
3231
3259
|
const filePath = this.#path("staging", fileName);
|
|
3260
|
+
return withEvolutionFileIdentity(this.#fs, filePath, (_full, sameStable) =>
|
|
3261
|
+
this.#writeTemporaryWithIdentity(filePath, value, sameStable),
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
#writeTemporaryWithIdentity(filePath, value, sameStable) {
|
|
3232
3266
|
const bytes = serialize(value);
|
|
3233
3267
|
let descriptor = null;
|
|
3234
3268
|
let writtenIdentity = null;
|
|
@@ -3248,7 +3282,7 @@ export class SkillReleaseRegistry {
|
|
|
3248
3282
|
"temporary write was incomplete",
|
|
3249
3283
|
);
|
|
3250
3284
|
}
|
|
3251
|
-
writtenIdentity =
|
|
3285
|
+
writtenIdentity = written;
|
|
3252
3286
|
} finally {
|
|
3253
3287
|
if (descriptor !== null) this.#fs.closeSync(descriptor);
|
|
3254
3288
|
}
|
|
@@ -3263,7 +3297,7 @@ export class SkillReleaseRegistry {
|
|
|
3263
3297
|
!staged.isFile() ||
|
|
3264
3298
|
staged.isSymbolicLink() ||
|
|
3265
3299
|
Number(staged.nlink) !== 1 ||
|
|
3266
|
-
|
|
3300
|
+
!sameStable(staged, writtenIdentity) ||
|
|
3267
3301
|
staged.size !== bytes.length ||
|
|
3268
3302
|
!samePath(realpath(this.#fs, filePath), filePath)
|
|
3269
3303
|
) {
|
|
@@ -4006,8 +4040,8 @@ export class SkillReleaseRegistry {
|
|
|
4006
4040
|
}
|
|
4007
4041
|
const afterReleases = this.#readInventoryNames("artifacts", releasePattern);
|
|
4008
4042
|
const afterStates = this.#readInventoryNames("active", statePattern);
|
|
4009
|
-
const afterStateDigests = afterStates.map(
|
|
4010
|
-
this.readState(name.slice(0, -".json".length)).stateDigest,
|
|
4043
|
+
const afterStateDigests = afterStates.map(
|
|
4044
|
+
(name) => this.readState(name.slice(0, -".json".length)).stateDigest,
|
|
4011
4045
|
);
|
|
4012
4046
|
if (
|
|
4013
4047
|
canonicalJson(beforeReleases) !== canonicalJson(afterReleases) ||
|