chainlesschain 0.166.13 → 0.166.14
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-BN25cNOD.js → AIOps-C8SyFhOo.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Da9IP-m5.js → ActionButton-Dup5gFK0.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-D7hkEvQm.js → Analytics-8FyOWA7V.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-CCV4Xnbc.js → AppLayout-DBzdU9dg.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-CVSnVJeA.js → Artifacts-C_0Z8gNy.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-CuYdw5Ag.js → Audit-Cf2a-58m.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-DZCnK-Tf.js → BackgroundAgents-M7w-2zgB.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-cV9FdpTy.js → Backup-CCTHQXoL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BRnv6YSp.js → BaseInput-DjXpeFwJ.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Bw5w87P4.js → Chat-cAvchsJU.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-C0GjtUAw.js → ChatBubbleRenderer-DvEvxlZv.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DaoTqcdt.js → Checkbox-DKs6qvOn.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B4dYcEIi.js → Codegen-DlkA7b9X.js} +1 -1
- package/src/assets/web-panel/assets/{Col-mAhuOeuD.js → Col-CkfdKhyJ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-BK6KEdBk.js → Community-iNXMHzj6.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CCglmNu5.js → Compact-DQxPEd09.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Cm8S4of7.js → Compliance-CSKvMCjs.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Dhs1QLMe.js → Cowork-CkWqazpD.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CLlhmXWk.js → Cron-k6W8IzhB.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BnHVbaNs.js → Crosschain-C8MS1OuY.js} +1 -1
- package/src/assets/web-panel/assets/{DID-UzQP9-0O.js → DID-DJuCcgiQ.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CBdlPClz.js → Dashboard-LeThKzp3.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-axIRPdq1.js → Dropdown-BvxwgpE4.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DUalW5rx.js → EmailListRenderer-n3e_a1M3.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-Bl4W9luY.js → FamilyGuardDashboard-509W6W6v.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C1yP-uEu.js → Federation-xgZReQm0.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Xh6Ig5q8.js → FormItemContext-jZOhKcQx.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-C2cz3bt2.js → GenericCardRenderer-Jo0ULzHv.js} +1 -1
- package/src/assets/web-panel/assets/{Git-C_z63ixg.js → Git-BsRcyWQP.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BYqB33_3.js → Governance-JlpMYTAk.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CEULuArm.js → Inference-CUEz872-.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-fgMsu-0H.js → KnowledgeGraph-DgrvvEDU.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DO9Hdcec.js → Logs-Df-KY4U3.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BEPDcSwz.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-B8zyLiFe.js → Marketplace-kPZDTOeI.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BLAN2mWW.js → McpTools-CRRzPqNX.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CrQcFM7u.js → Memory-D6pNSkg9.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DVnyiiU-.js → MobileBridge-g-pvtwRC.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-qjCME5OD.js → MobileProjects-Bzayfrfe.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-4JE3C3It.js → Mtc-B7S3W-XL.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-CjBuak9I.js → MtcAudit-B8OMEWVb.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-BtPcYQcC.js → Multisig-CowRp7GH.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-Cx653_Ba.js → NLProgramming-BkQNyi4c.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CkuD3YzS.js → Notes-Cm7gtkKz.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BgNgU7HH.js → NotificationSettings-CbPTVh9m.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BTC8W0a-.js → OrderTableRenderer-Dswi26mJ.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-Dmd4Hjxz.js → Organization-BkjQ9yy7.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-C_5DA-G0.js → Overflow-DmRFXP7Y.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-uwslV4XC.js → P2P-DQE1toAu.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CuNgwVnX.js → PdhVaultBrowser-BujyoDRG.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-CJayVX9H.js → Permissions-xSjfOaiU.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-Cb05H5go.js → PersonalDataHub-B9ICLqVP.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-BZlGKapZ.js → Pipeline-CUNwyN9E.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BIzZWYzd.js → Privacy-SOoCricX.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-vEYYyQXQ.js → ProjectInit-CSaFul0F.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-MZqTy5CC.js → ProjectSettings-CueiB_oe.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-CRARUOl8.js → Projects-BOeXvU7y.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-80VAK8PX.js → Providers-CTqf_31h.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CzRSoIwK.js → QrScannerModal-C8ZP31E9.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-BpwQ2n4C.js → QuickAsk-CPgn4713.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BC03TZ79.js → Recommend-8_e0zw0i.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DE59-jaa.js → RemoteSession-iT7lbGqw.js} +3 -3
- package/src/assets/web-panel/assets/{Reputation-DOHmeWyA.js → Reputation-B4q9SUOz.js} +1 -1
- package/src/assets/web-panel/assets/{Row-hc5RZl1D.js → Row-CA_nASyL.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Cv5uMP7E.js → RssFeed-Cwb0E3yv.js} +3 -3
- package/src/assets/web-panel/assets/{Search-qohu7Ug0.js → Search-D9iVsOxJ.js} +1 -1
- package/src/assets/web-panel/assets/{Security-drCPFWV5.js → Security-DPLbYchG.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BAEgUeRy.js → Services-DGMVL_LF.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CkJkKBlo.js → Skeleton-B_6kym0D.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-bMuKEpez.js → Skills-Ds6r0dXB.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DxWQVHRC.js → Sla-BLHAbBhb.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BpS8idmV.js → SpeechSettings-CZuddd14.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-BPBLlW5v.js → SyncSettings-CdBq7oSl.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-C4SLEeUh.js → Tasks-DfT60Nyl.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-r-wfU39U.js → Templates-RLfsKy2V.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-C7vjRmLN.js → Tenant-Cw8xCNLE.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DdzeFCV2.js → Terminal-BOUkNu4f.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CYyWN2KD.js → TimelineRenderer-CgGZWaRm.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-D8A9osUg.js → Tokens-CpwjVyir.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DBvrNlyS.js → Trigger-DL21H-eb.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DBVAGc7o.js → Trust-B3ffFclW.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-C1vQUCRJ.js → UkeySign-CkBeb5aI.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BKs4dah4.js → VideoEditing-U2iVaE3r.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-ByIuj3lD.js → Wallet-CQk2wzYd.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DYlAFCm9.js → WebAuthn-DYIuIYzT.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-Dwyadd0c.js → WorkflowEditor-tg1c2mZQ.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Bm2f3s08.js → chat-CtL61spq.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C8ORSh7V.js → colors-DKpbeo8s.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-1iVlnsM-.js → compact-item-gPZsrswt.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CmqNyUSL.js → createContext-CfHlwzVr.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DdqnMhzi.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-rUMaX3Ul.js → hasIn-MV47g_6X.js} +1 -1
- package/src/assets/web-panel/assets/{index-CCEyz58d.js → index-6G0ETFTW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVtnizyC.js → index-BBw52sEl.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIvM-uAX.js → index-BN-wxSQ8.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvedhJeM.js → index-BOyGUuBE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCOhTILd.js → index-BQp5M0s1.js} +1 -1
- package/src/assets/web-panel/assets/index-BcK8o4ke.js +1 -0
- package/src/assets/web-panel/assets/{index-DVJ1uKf4.js → index-BjlPD7GO.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQL9r73Y.js → index-C4lF-f_z.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5RfRciU.js → index-C6nC_DyY.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgUE51Pz.js → index-C9RF4ieu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkFvuAex.js → index-C9gtY4nG.js} +3 -3
- package/src/assets/web-panel/assets/{index-C-J5-pGH.js → index-CB1HnbNK.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bf1PlIHp.js → index-CRv_uMLV.js} +1 -1
- package/src/assets/web-panel/assets/{index-tqTx5wXo.js → index-C_B9LiKs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bgw4Y2rU.js → index-Cjv0kKCU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJxtPFOh.js → index-CmM9jqwe.js} +1 -1
- package/src/assets/web-panel/assets/{index-pHWA7bmM.js → index-CmPJpLoD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dsve5Gkv.js → index-CzfxTSPW.js} +1 -1
- package/src/assets/web-panel/assets/{index-B59OWAs7.js → index-D1XB5-t5.js} +1 -1
- package/src/assets/web-panel/assets/{index-BK-RY0-7.js → index-DGtEBodr.js} +1 -1
- package/src/assets/web-panel/assets/{index-BC32_0xT.js → index-DIeJmlAA.js} +1 -1
- package/src/assets/web-panel/assets/{index-OcbzdRpT.js → index-DMMvlLp9.js} +1 -1
- package/src/assets/web-panel/assets/{index-geQinnns.js → index-DU4rZpiK.js} +1 -1
- package/src/assets/web-panel/assets/{index-D98FXb0w.js → index-D_Pl1GnW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BC8Wb6cy.js → index-Dd19vRwy.js} +1 -1
- package/src/assets/web-panel/assets/{index-fPK6GFEg.js → index-Ddcpj1El.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfiHyVyl.js → index-Df9sWKpl.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-ZIHi42.js → index-DfLyHCWi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDHk911Q.js → index-DnZ_zS1M.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFniLMn9.js → index-DsH-i_3d.js} +1 -1
- package/src/assets/web-panel/assets/index-DvbAjyYI.js +1 -0
- package/src/assets/web-panel/assets/{index-Dcck8QfW.js → index-ExAGPAnX.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwMdWKiF.js → index-Rb7CRMw9.js} +1 -1
- package/src/assets/web-panel/assets/{index-reUHhuz8.js → index-Zvkt9xqi.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgwLNxmr.js → index-bSFrAszw.js} +1 -1
- package/src/assets/web-panel/assets/{index-C3B1S-mW.js → index-fYcS7NS0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CTfEmKK0.js → index-srBc9U_s.js} +1 -1
- package/src/assets/web-panel/assets/{index-B53c3diB.js → index-xUlgG0Xe.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8BrB0Ls.js → index-yxxXlqBA.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CuA-cpkL.js → initDefaultProps-BeOZoSZ4.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CimmE_9b.js → motion-BXE8VMKx.js} +1 -1
- package/src/assets/web-panel/assets/{move-DlGlrltz.js → move-I9sdvPGX.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CK6BJQ07.js → mtc-parser-d25zrJd0.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BKjBfZfB.js → omit-D71V_T4e.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B2chVYw9.js → pickAttrs-BNZKpIAR.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BW8aLJJ3.js → placementArrow-jOcgxTTz.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-dXlPl66S.js → responsiveObserve-pta_O8AU.js} +1 -1
- package/src/assets/web-panel/assets/{slide-hSa89S4G.js → slide-Dlq6om7z.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DM6KLt-s.js → statusUtils-BnkO_yRv.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-C9MQAK15.js → styleChecker-p8NfWmcG.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BpQDpP0z.js → useFlexGapSupport-KcjZZ5U7.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C7rtRViA.js → useFs-gqonDMWB.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Dm15xafq.js → usePersonalDataHub-jIIfE4go.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-mEiEytw4.js → vnode-Dyo7fBrM.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DzPxflhY.js → zoom-6l8BMg9W.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +2 -2
- package/src/commands/agent.js +49 -27
- package/src/commands/record-replay.js +772 -0
- package/src/commands/skill.js +3 -0
- package/src/data/changelog.json +13 -0
- package/src/lib/agent-sandbox.js +4 -8
- package/src/lib/agent-team/team-message-tools.js +23 -5
- package/src/lib/process-execution-broker/credential-transport.js +31 -0
- package/src/lib/process-execution-broker/index.js +74 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.dll +0 -0
- package/src/lib/process-execution-broker/windows-sandbox-helper.exe +0 -0
- package/src/lib/process-execution-broker/windows-sandbox.cs +7 -3
- package/src/lib/record-replay/browser-target-policy.js +406 -0
- package/src/lib/record-replay/index.js +4 -0
- package/src/lib/record-replay/playwright-ui-driver.js +83 -20
- package/src/lib/record-replay/playwright-ui-recorder.js +611 -0
- package/src/lib/record-replay/recorded-skill-package.js +486 -0
- package/src/lib/record-replay/recorded-skill-store.js +1166 -0
- package/src/lib/record-replay/skill-recorder.js +634 -64
- package/src/runtime/headless-runner.js +72 -51
- package/src/assets/web-panel/assets/MarkdownRenderer-BkrK04xW.js +0 -1
- package/src/assets/web-panel/assets/devWarning-B69RCDDH.js +0 -1
- package/src/assets/web-panel/assets/index-9biMQ-tw.js +0 -1
- package/src/assets/web-panel/assets/index-COie87kl.js +0 -1
|
@@ -0,0 +1,1166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
fsyncSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
import { getHomeDir } from "../paths.js";
|
|
16
|
+
import { ensurePrivateDirectory, ensurePrivateFile } from "../secure-fs.js";
|
|
17
|
+
import { withFileLock } from "../with-file-lock.js";
|
|
18
|
+
import {
|
|
19
|
+
validateRecordedSkillDraft,
|
|
20
|
+
validateRecordedSkillReplayReport,
|
|
21
|
+
validateReviewedRecordedSkill,
|
|
22
|
+
} from "./skill-recorder.js";
|
|
23
|
+
import { createRecordedSkillNetworkPolicy } from "./browser-target-policy.js";
|
|
24
|
+
|
|
25
|
+
const STORE_SCHEMA = "chainlesschain.recorded-skill-store/v1";
|
|
26
|
+
const ENTRY_SCHEMA = "chainlesschain.recorded-skill-entry/v1";
|
|
27
|
+
const EXPORT_SCHEMA = "chainlesschain.recorded-skill-export/v1";
|
|
28
|
+
const POLICY_SCHEMA = "chainlesschain.recorded-skill-policy/v1";
|
|
29
|
+
const AUDIT_SCHEMA = "chainlesschain.recorded-skill-audit-event/v1";
|
|
30
|
+
const STORE_DIGEST_DOMAIN = "cc.record-replay.store/v1";
|
|
31
|
+
const ENTRY_DIGEST_DOMAIN = "cc.record-replay.store-entry/v1";
|
|
32
|
+
const EXPORT_DIGEST_DOMAIN = "cc.record-replay.export/v1";
|
|
33
|
+
const AUDIT_DIGEST_DOMAIN = "cc.record-replay.audit-event/v1";
|
|
34
|
+
const MAX_STORE_BYTES = 32 * 1024 * 1024;
|
|
35
|
+
const STATES = new Set([
|
|
36
|
+
"draft",
|
|
37
|
+
"approved",
|
|
38
|
+
"validated",
|
|
39
|
+
"enabled",
|
|
40
|
+
"revoked",
|
|
41
|
+
]);
|
|
42
|
+
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_RECORDED_SKILL_POLICY = Object.freeze({
|
|
45
|
+
schema: POLICY_SCHEMA,
|
|
46
|
+
retentionDays: 90,
|
|
47
|
+
maxRecords: 500,
|
|
48
|
+
maxActions: 256,
|
|
49
|
+
maxAuditEvents: 20_000,
|
|
50
|
+
allowedCapabilities: Object.freeze(["ui.interact", "ui.observe"]),
|
|
51
|
+
allowGlobalInstall: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function storeError(code, message, details = {}) {
|
|
55
|
+
const error = new Error(message);
|
|
56
|
+
error.name = "RecordedSkillStoreError";
|
|
57
|
+
error.code = code;
|
|
58
|
+
Object.assign(error, details);
|
|
59
|
+
return error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function canonicalValue(value) {
|
|
63
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
64
|
+
if (!value || typeof value !== "object") return value;
|
|
65
|
+
return Object.fromEntries(
|
|
66
|
+
Object.keys(value)
|
|
67
|
+
.sort()
|
|
68
|
+
.map((key) => [key, canonicalValue(value[key])]),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function digest(value, domain) {
|
|
73
|
+
return `sha256:${createHash("sha256")
|
|
74
|
+
.update(domain)
|
|
75
|
+
.update("\0")
|
|
76
|
+
.update(JSON.stringify(canonicalValue(value)))
|
|
77
|
+
.digest("hex")}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function clone(value) {
|
|
81
|
+
return JSON.parse(JSON.stringify(value));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function deepFreeze(value) {
|
|
85
|
+
if (!value || typeof value !== "object") return value;
|
|
86
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
87
|
+
return Object.freeze(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertObject(value, code, label) {
|
|
91
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
92
|
+
throw storeError(code, `${label} must be an object`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertExactKeys(value, keys, code, label) {
|
|
98
|
+
if (Object.keys(value).some((key) => !keys.has(key))) {
|
|
99
|
+
throw storeError(code, `${label} contains unsupported fields`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertName(value) {
|
|
104
|
+
const name = String(value || "").trim();
|
|
105
|
+
if (!/^[A-Za-z][A-Za-z0-9._-]{0,127}$/u.test(name)) {
|
|
106
|
+
throw storeError(
|
|
107
|
+
"CC_RECORD_STORE_ARGUMENT_INVALID",
|
|
108
|
+
"recorded skill name is invalid",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return name;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function assertTimestamp(value, label) {
|
|
115
|
+
const text = String(value || "");
|
|
116
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(text)) {
|
|
117
|
+
throw storeError("CC_RECORD_STORE_CORRUPT", `${label} is invalid`);
|
|
118
|
+
}
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assertRevision(value, label = "revision") {
|
|
123
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
124
|
+
throw storeError("CC_RECORD_STORE_CORRUPT", `${label} is invalid`);
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizePolicy(value) {
|
|
130
|
+
assertObject(value, "CC_RECORD_POLICY_INVALID", "recorded skill policy");
|
|
131
|
+
assertExactKeys(
|
|
132
|
+
value,
|
|
133
|
+
new Set([
|
|
134
|
+
"schema",
|
|
135
|
+
"retentionDays",
|
|
136
|
+
"maxRecords",
|
|
137
|
+
"maxActions",
|
|
138
|
+
"maxAuditEvents",
|
|
139
|
+
"allowedCapabilities",
|
|
140
|
+
"allowGlobalInstall",
|
|
141
|
+
]),
|
|
142
|
+
"CC_RECORD_POLICY_INVALID",
|
|
143
|
+
"recorded skill policy",
|
|
144
|
+
);
|
|
145
|
+
const policy = {
|
|
146
|
+
schema: POLICY_SCHEMA,
|
|
147
|
+
retentionDays: Number(value.retentionDays),
|
|
148
|
+
maxRecords: Number(value.maxRecords),
|
|
149
|
+
maxActions: Number(value.maxActions),
|
|
150
|
+
maxAuditEvents: Number(value.maxAuditEvents),
|
|
151
|
+
allowedCapabilities: [...new Set(value.allowedCapabilities || [])].sort(),
|
|
152
|
+
allowGlobalInstall: value.allowGlobalInstall === true,
|
|
153
|
+
};
|
|
154
|
+
if (
|
|
155
|
+
value.schema !== POLICY_SCHEMA ||
|
|
156
|
+
!Number.isInteger(policy.retentionDays) ||
|
|
157
|
+
policy.retentionDays < 1 ||
|
|
158
|
+
policy.retentionDays > 3_650 ||
|
|
159
|
+
!Number.isInteger(policy.maxRecords) ||
|
|
160
|
+
policy.maxRecords < 1 ||
|
|
161
|
+
policy.maxRecords > 10_000 ||
|
|
162
|
+
!Number.isInteger(policy.maxActions) ||
|
|
163
|
+
policy.maxActions < 1 ||
|
|
164
|
+
policy.maxActions > 256 ||
|
|
165
|
+
!Number.isInteger(policy.maxAuditEvents) ||
|
|
166
|
+
policy.maxAuditEvents < 100 ||
|
|
167
|
+
policy.maxAuditEvents > 100_000 ||
|
|
168
|
+
policy.allowedCapabilities.length < 1 ||
|
|
169
|
+
policy.allowedCapabilities.some(
|
|
170
|
+
(capability) => !["ui.interact", "ui.observe"].includes(capability),
|
|
171
|
+
)
|
|
172
|
+
) {
|
|
173
|
+
throw storeError(
|
|
174
|
+
"CC_RECORD_POLICY_INVALID",
|
|
175
|
+
"recorded skill policy is invalid",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return deepFreeze(policy);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeSource(value) {
|
|
182
|
+
assertObject(value, "CC_RECORD_STORE_CORRUPT", "recording source");
|
|
183
|
+
assertExactKeys(
|
|
184
|
+
value,
|
|
185
|
+
new Set(["adapter", "targetDigest", "browserVersion"]),
|
|
186
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
187
|
+
"recording source",
|
|
188
|
+
);
|
|
189
|
+
if (
|
|
190
|
+
!["self-contained-html", "url-origin"].includes(value.adapter) ||
|
|
191
|
+
!SHA256_PATTERN.test(String(value.targetDigest || ""))
|
|
192
|
+
) {
|
|
193
|
+
throw storeError("CC_RECORD_STORE_CORRUPT", "recording source is invalid");
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
adapter: value.adapter,
|
|
197
|
+
targetDigest: value.targetDigest,
|
|
198
|
+
browserVersion: String(value.browserVersion || "unknown").slice(0, 128),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function assertBrowserSourceBinding(
|
|
203
|
+
skill,
|
|
204
|
+
source,
|
|
205
|
+
code = "CC_RECORD_STORE_CORRUPT",
|
|
206
|
+
) {
|
|
207
|
+
const requirements = skill.environment.requirements;
|
|
208
|
+
const policy = requirements.networkPolicy;
|
|
209
|
+
let normalizedPolicy;
|
|
210
|
+
try {
|
|
211
|
+
normalizedPolicy = createRecordedSkillNetworkPolicy({
|
|
212
|
+
mode: policy?.mode,
|
|
213
|
+
allowedOrigins: policy?.allowedOrigins || [],
|
|
214
|
+
});
|
|
215
|
+
} catch {
|
|
216
|
+
normalizedPolicy = null;
|
|
217
|
+
}
|
|
218
|
+
const storageStateDigest = requirements.storageStateDigest;
|
|
219
|
+
const bindingValid =
|
|
220
|
+
requirements.adapter === source.adapter &&
|
|
221
|
+
requirements.targetDigest === source.targetDigest &&
|
|
222
|
+
requirements.browser === "chromium" &&
|
|
223
|
+
requirements.selectorContract === "record-replay-dom-v1" &&
|
|
224
|
+
/^[A-Za-z][A-Za-z0-9._-]{0,127}$/u.test(
|
|
225
|
+
String(requirements.identity || ""),
|
|
226
|
+
) &&
|
|
227
|
+
(storageStateDigest == null ||
|
|
228
|
+
SHA256_PATTERN.test(String(storageStateDigest))) &&
|
|
229
|
+
normalizedPolicy?.digest === policy?.digest &&
|
|
230
|
+
((source.adapter === "self-contained-html" &&
|
|
231
|
+
normalizedPolicy?.mode === "deny" &&
|
|
232
|
+
storageStateDigest == null) ||
|
|
233
|
+
(source.adapter === "url-origin" &&
|
|
234
|
+
normalizedPolicy?.mode === "allowlist"));
|
|
235
|
+
if (!bindingValid) {
|
|
236
|
+
throw storeError(
|
|
237
|
+
code,
|
|
238
|
+
"recording source does not match its browser environment binding",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeLastReplay(value, skill) {
|
|
244
|
+
if (value == null) return null;
|
|
245
|
+
assertObject(value, "CC_RECORD_STORE_CORRUPT", "last replay");
|
|
246
|
+
assertExactKeys(
|
|
247
|
+
value,
|
|
248
|
+
new Set(["report", "targetDigest", "browserVersion", "durationMs", "at"]),
|
|
249
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
250
|
+
"last replay",
|
|
251
|
+
);
|
|
252
|
+
const report = validateRecordedSkillReplayReport(value.report, { skill });
|
|
253
|
+
if (!SHA256_PATTERN.test(String(value.targetDigest || ""))) {
|
|
254
|
+
throw storeError(
|
|
255
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
256
|
+
"last replay target digest is invalid",
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const durationMs = Number(value.durationMs);
|
|
260
|
+
if (
|
|
261
|
+
!Number.isInteger(durationMs) ||
|
|
262
|
+
durationMs < 0 ||
|
|
263
|
+
durationMs > 86_400_000
|
|
264
|
+
) {
|
|
265
|
+
throw storeError(
|
|
266
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
267
|
+
"last replay duration is invalid",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
report,
|
|
272
|
+
targetDigest: value.targetDigest,
|
|
273
|
+
browserVersion: String(value.browserVersion || "unknown").slice(0, 128),
|
|
274
|
+
durationMs,
|
|
275
|
+
at: assertTimestamp(value.at, "last replay timestamp"),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeInstallation(value) {
|
|
280
|
+
if (value == null) return null;
|
|
281
|
+
assertObject(value, "CC_RECORD_STORE_CORRUPT", "recorded skill installation");
|
|
282
|
+
assertExactKeys(
|
|
283
|
+
value,
|
|
284
|
+
new Set(["scope", "packageDigest", "installedAt"]),
|
|
285
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
286
|
+
"recorded skill installation",
|
|
287
|
+
);
|
|
288
|
+
if (
|
|
289
|
+
!["project", "global"].includes(value.scope) ||
|
|
290
|
+
!SHA256_PATTERN.test(String(value.packageDigest || ""))
|
|
291
|
+
) {
|
|
292
|
+
throw storeError(
|
|
293
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
294
|
+
"recorded skill installation is invalid",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
scope: value.scope,
|
|
299
|
+
packageDigest: value.packageDigest,
|
|
300
|
+
installedAt: assertTimestamp(value.installedAt, "installation timestamp"),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function entryBody(value) {
|
|
305
|
+
return {
|
|
306
|
+
schema: ENTRY_SCHEMA,
|
|
307
|
+
name: value.name,
|
|
308
|
+
revision: value.revision,
|
|
309
|
+
state: value.state,
|
|
310
|
+
createdAt: value.createdAt,
|
|
311
|
+
updatedAt: value.updatedAt,
|
|
312
|
+
expiresAt: value.expiresAt,
|
|
313
|
+
source: value.source,
|
|
314
|
+
skill: value.skill,
|
|
315
|
+
lastReplay: value.lastReplay,
|
|
316
|
+
installation: value.installation,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function normalizeEntry(value) {
|
|
321
|
+
assertObject(value, "CC_RECORD_STORE_CORRUPT", "recorded skill entry");
|
|
322
|
+
assertExactKeys(
|
|
323
|
+
value,
|
|
324
|
+
new Set([
|
|
325
|
+
"schema",
|
|
326
|
+
"name",
|
|
327
|
+
"revision",
|
|
328
|
+
"state",
|
|
329
|
+
"createdAt",
|
|
330
|
+
"updatedAt",
|
|
331
|
+
"expiresAt",
|
|
332
|
+
"source",
|
|
333
|
+
"skill",
|
|
334
|
+
"lastReplay",
|
|
335
|
+
"installation",
|
|
336
|
+
"entryDigest",
|
|
337
|
+
]),
|
|
338
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
339
|
+
"recorded skill entry",
|
|
340
|
+
);
|
|
341
|
+
const name = assertName(value.name);
|
|
342
|
+
const state = String(value.state || "");
|
|
343
|
+
if (value.schema !== ENTRY_SCHEMA || !STATES.has(state)) {
|
|
344
|
+
throw storeError(
|
|
345
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
346
|
+
"recorded skill entry schema or state is invalid",
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const skill =
|
|
350
|
+
state === "draft"
|
|
351
|
+
? validateRecordedSkillDraft(value.skill)
|
|
352
|
+
: validateReviewedRecordedSkill(value.skill);
|
|
353
|
+
if (skill.name !== name) {
|
|
354
|
+
throw storeError(
|
|
355
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
356
|
+
"recorded skill entry name does not match its skill",
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const source = normalizeSource(value.source);
|
|
360
|
+
assertBrowserSourceBinding(skill, source);
|
|
361
|
+
const body = {
|
|
362
|
+
schema: ENTRY_SCHEMA,
|
|
363
|
+
name,
|
|
364
|
+
revision: assertRevision(value.revision),
|
|
365
|
+
state,
|
|
366
|
+
createdAt: assertTimestamp(value.createdAt, "creation timestamp"),
|
|
367
|
+
updatedAt: assertTimestamp(value.updatedAt, "update timestamp"),
|
|
368
|
+
expiresAt: assertTimestamp(value.expiresAt, "expiry timestamp"),
|
|
369
|
+
source,
|
|
370
|
+
skill,
|
|
371
|
+
lastReplay: normalizeLastReplay(value.lastReplay, skill),
|
|
372
|
+
installation: normalizeInstallation(value.installation),
|
|
373
|
+
};
|
|
374
|
+
if (["validated", "enabled", "revoked"].includes(state) && !body.lastReplay) {
|
|
375
|
+
throw storeError(
|
|
376
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
377
|
+
"validated recorded skill is missing replay evidence",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (
|
|
381
|
+
body.lastReplay &&
|
|
382
|
+
body.lastReplay.targetDigest !== body.source.targetDigest
|
|
383
|
+
) {
|
|
384
|
+
throw storeError(
|
|
385
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
386
|
+
"recorded skill replay target does not match its source",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
if (state === "enabled" && !body.installation) {
|
|
390
|
+
throw storeError(
|
|
391
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
392
|
+
"enabled recorded skill is missing installation evidence",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (state !== "enabled" && body.installation) {
|
|
396
|
+
throw storeError(
|
|
397
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
398
|
+
"inactive recorded skill retains active installation state",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const expectedDigest = digest(body, ENTRY_DIGEST_DOMAIN);
|
|
402
|
+
if (value.entryDigest !== expectedDigest) {
|
|
403
|
+
throw storeError(
|
|
404
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
405
|
+
"recorded skill entry digest is invalid",
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return deepFreeze({ ...clone(body), entryDigest: expectedDigest });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function sealEntry(value) {
|
|
412
|
+
const body = entryBody(value);
|
|
413
|
+
return normalizeEntry({
|
|
414
|
+
...clone(body),
|
|
415
|
+
entryDigest: digest(body, ENTRY_DIGEST_DOMAIN),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function auditBody(value) {
|
|
420
|
+
return {
|
|
421
|
+
schema: AUDIT_SCHEMA,
|
|
422
|
+
sequence: value.sequence,
|
|
423
|
+
eventId: value.eventId,
|
|
424
|
+
at: value.at,
|
|
425
|
+
action: value.action,
|
|
426
|
+
name: value.name,
|
|
427
|
+
revision: value.revision,
|
|
428
|
+
state: value.state,
|
|
429
|
+
actor: value.actor,
|
|
430
|
+
entryDigest: value.entryDigest,
|
|
431
|
+
previousDigest: value.previousDigest,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function normalizeAudit(events) {
|
|
436
|
+
if (!Array.isArray(events)) {
|
|
437
|
+
throw storeError(
|
|
438
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
439
|
+
"recorded skill audit log is invalid",
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
let previousDigest = null;
|
|
443
|
+
return events.map((value, index) => {
|
|
444
|
+
assertObject(
|
|
445
|
+
value,
|
|
446
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
447
|
+
"recorded skill audit event",
|
|
448
|
+
);
|
|
449
|
+
assertExactKeys(
|
|
450
|
+
value,
|
|
451
|
+
new Set([
|
|
452
|
+
"schema",
|
|
453
|
+
"sequence",
|
|
454
|
+
"eventId",
|
|
455
|
+
"at",
|
|
456
|
+
"action",
|
|
457
|
+
"name",
|
|
458
|
+
"revision",
|
|
459
|
+
"state",
|
|
460
|
+
"actor",
|
|
461
|
+
"entryDigest",
|
|
462
|
+
"previousDigest",
|
|
463
|
+
"eventDigest",
|
|
464
|
+
]),
|
|
465
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
466
|
+
"recorded skill audit event",
|
|
467
|
+
);
|
|
468
|
+
const body = auditBody({
|
|
469
|
+
schema: value.schema,
|
|
470
|
+
sequence: value.sequence,
|
|
471
|
+
eventId: value.eventId,
|
|
472
|
+
at: value.at,
|
|
473
|
+
action: value.action,
|
|
474
|
+
name: value.name,
|
|
475
|
+
revision: value.revision,
|
|
476
|
+
state: value.state,
|
|
477
|
+
actor: value.actor,
|
|
478
|
+
entryDigest: value.entryDigest,
|
|
479
|
+
previousDigest: value.previousDigest,
|
|
480
|
+
});
|
|
481
|
+
if (
|
|
482
|
+
value.schema !== AUDIT_SCHEMA ||
|
|
483
|
+
value.sequence !== index + 1 ||
|
|
484
|
+
!/^[a-f0-9-]{36}$/u.test(String(value.eventId || "")) ||
|
|
485
|
+
!/^[a-z][a-z0-9_-]{1,63}$/u.test(String(value.action || "")) ||
|
|
486
|
+
value.name !== assertName(value.name) ||
|
|
487
|
+
!Number.isInteger(value.revision) ||
|
|
488
|
+
value.revision < 0 ||
|
|
489
|
+
![...STATES, "deleted", "policy"].includes(value.state) ||
|
|
490
|
+
typeof value.actor !== "string" ||
|
|
491
|
+
value.actor.length < 1 ||
|
|
492
|
+
value.actor.length > 128 ||
|
|
493
|
+
(value.entryDigest !== null &&
|
|
494
|
+
!SHA256_PATTERN.test(String(value.entryDigest))) ||
|
|
495
|
+
value.previousDigest !== previousDigest ||
|
|
496
|
+
value.eventDigest !== digest(body, AUDIT_DIGEST_DOMAIN)
|
|
497
|
+
) {
|
|
498
|
+
throw storeError(
|
|
499
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
500
|
+
"recorded skill audit chain is invalid",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
assertTimestamp(value.at, "audit timestamp");
|
|
504
|
+
previousDigest = value.eventDigest;
|
|
505
|
+
return deepFreeze({ ...body, eventDigest: value.eventDigest });
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function storeBody(value) {
|
|
510
|
+
return {
|
|
511
|
+
schema: STORE_SCHEMA,
|
|
512
|
+
revision: value.revision,
|
|
513
|
+
policy: value.policy,
|
|
514
|
+
records: value.records,
|
|
515
|
+
audit: value.audit,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function normalizeStore(value) {
|
|
520
|
+
assertObject(value, "CC_RECORD_STORE_CORRUPT", "recorded skill store");
|
|
521
|
+
assertExactKeys(
|
|
522
|
+
value,
|
|
523
|
+
new Set([
|
|
524
|
+
"schema",
|
|
525
|
+
"revision",
|
|
526
|
+
"policy",
|
|
527
|
+
"records",
|
|
528
|
+
"audit",
|
|
529
|
+
"storeDigest",
|
|
530
|
+
]),
|
|
531
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
532
|
+
"recorded skill store",
|
|
533
|
+
);
|
|
534
|
+
if (
|
|
535
|
+
value.schema !== STORE_SCHEMA ||
|
|
536
|
+
!Number.isInteger(value.revision) ||
|
|
537
|
+
value.revision < 0
|
|
538
|
+
) {
|
|
539
|
+
throw storeError(
|
|
540
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
541
|
+
"recorded skill store schema or revision is invalid",
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const policy = normalizePolicy(value.policy);
|
|
545
|
+
assertObject(
|
|
546
|
+
value.records,
|
|
547
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
548
|
+
"recorded skill records",
|
|
549
|
+
);
|
|
550
|
+
const records = Object.fromEntries(
|
|
551
|
+
Object.entries(value.records).map(([name, entry]) => {
|
|
552
|
+
if (name !== assertName(name)) {
|
|
553
|
+
throw storeError(
|
|
554
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
555
|
+
"recorded skill record key is invalid",
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
const normalized = normalizeEntry(entry);
|
|
559
|
+
if (normalized.name !== name) {
|
|
560
|
+
throw storeError(
|
|
561
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
562
|
+
"recorded skill record key does not match entry",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
return [name, normalized];
|
|
566
|
+
}),
|
|
567
|
+
);
|
|
568
|
+
if (
|
|
569
|
+
Object.keys(records).length > policy.maxRecords ||
|
|
570
|
+
Object.values(records).some(
|
|
571
|
+
(entry) => entry.skill.actions.length > policy.maxActions,
|
|
572
|
+
)
|
|
573
|
+
) {
|
|
574
|
+
throw storeError(
|
|
575
|
+
"CC_RECORD_POLICY_DENIED",
|
|
576
|
+
"recorded skill store exceeds current policy",
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const audit = normalizeAudit(value.audit);
|
|
580
|
+
if (audit.length > policy.maxAuditEvents) {
|
|
581
|
+
throw storeError(
|
|
582
|
+
"CC_RECORD_POLICY_DENIED",
|
|
583
|
+
"recorded skill audit log exceeds current policy",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const body = {
|
|
587
|
+
schema: STORE_SCHEMA,
|
|
588
|
+
revision: value.revision,
|
|
589
|
+
policy,
|
|
590
|
+
records,
|
|
591
|
+
audit,
|
|
592
|
+
};
|
|
593
|
+
if (value.storeDigest !== digest(body, STORE_DIGEST_DOMAIN)) {
|
|
594
|
+
throw storeError(
|
|
595
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
596
|
+
"recorded skill store digest is invalid",
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
return deepFreeze({ ...clone(body), storeDigest: value.storeDigest });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function emptyStore() {
|
|
603
|
+
const body = {
|
|
604
|
+
schema: STORE_SCHEMA,
|
|
605
|
+
revision: 0,
|
|
606
|
+
policy: DEFAULT_RECORDED_SKILL_POLICY,
|
|
607
|
+
records: {},
|
|
608
|
+
audit: [],
|
|
609
|
+
};
|
|
610
|
+
return normalizeStore({
|
|
611
|
+
...clone(body),
|
|
612
|
+
storeDigest: digest(body, STORE_DIGEST_DOMAIN),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function appendAudit(document, { action, entry, name, actor, at, state }) {
|
|
617
|
+
if (document.audit.length >= document.policy.maxAuditEvents) {
|
|
618
|
+
throw storeError(
|
|
619
|
+
"CC_RECORD_POLICY_DENIED",
|
|
620
|
+
"recorded skill audit event limit reached",
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const previousDigest = document.audit.at(-1)?.eventDigest || null;
|
|
624
|
+
const body = {
|
|
625
|
+
schema: AUDIT_SCHEMA,
|
|
626
|
+
sequence: document.audit.length + 1,
|
|
627
|
+
eventId: randomUUID(),
|
|
628
|
+
at,
|
|
629
|
+
action,
|
|
630
|
+
name: assertName(name),
|
|
631
|
+
revision: entry?.revision || 0,
|
|
632
|
+
state: state || entry?.state || "deleted",
|
|
633
|
+
actor: String(actor || "cli").slice(0, 128),
|
|
634
|
+
entryDigest: entry?.entryDigest || null,
|
|
635
|
+
previousDigest,
|
|
636
|
+
};
|
|
637
|
+
document.audit.push({
|
|
638
|
+
...body,
|
|
639
|
+
eventDigest: digest(body, AUDIT_DIGEST_DOMAIN),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function summary(entry) {
|
|
644
|
+
return deepFreeze({
|
|
645
|
+
name: entry.name,
|
|
646
|
+
revision: entry.revision,
|
|
647
|
+
state: entry.state,
|
|
648
|
+
description: entry.skill.description,
|
|
649
|
+
actionCount: entry.skill.actions.length,
|
|
650
|
+
parameterCount: entry.skill.parameters.length,
|
|
651
|
+
capabilities: clone(entry.skill.capabilityManifest),
|
|
652
|
+
updatedAt: entry.updatedAt,
|
|
653
|
+
expiresAt: entry.expiresAt,
|
|
654
|
+
entryDigest: entry.entryDigest,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export class RecordedSkillStore {
|
|
659
|
+
constructor({
|
|
660
|
+
rootDir = join(getHomeDir(), "record-replay"),
|
|
661
|
+
now = () => new Date(),
|
|
662
|
+
secure = true,
|
|
663
|
+
} = {}) {
|
|
664
|
+
this.rootDir = resolve(rootDir);
|
|
665
|
+
this.filePath = join(this.rootDir, "state.json");
|
|
666
|
+
this._now = now;
|
|
667
|
+
this._secure = secure !== false;
|
|
668
|
+
if (this._secure) {
|
|
669
|
+
ensurePrivateDirectory(this.rootDir, { failIfUnavailable: true });
|
|
670
|
+
} else {
|
|
671
|
+
mkdirSync(this.rootDir, { recursive: true, mode: 0o700 });
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
_read() {
|
|
676
|
+
if (!existsSync(this.filePath)) return emptyStore();
|
|
677
|
+
const entry = lstatSync(this.filePath);
|
|
678
|
+
if (
|
|
679
|
+
!entry.isFile() ||
|
|
680
|
+
entry.isSymbolicLink() ||
|
|
681
|
+
entry.size > MAX_STORE_BYTES
|
|
682
|
+
) {
|
|
683
|
+
throw storeError(
|
|
684
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
685
|
+
"recorded skill store file is unsafe",
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
let parsed;
|
|
689
|
+
try {
|
|
690
|
+
parsed = JSON.parse(readFileSync(this.filePath, "utf8"));
|
|
691
|
+
} catch {
|
|
692
|
+
throw storeError(
|
|
693
|
+
"CC_RECORD_STORE_CORRUPT",
|
|
694
|
+
"recorded skill store is not valid JSON",
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
return normalizeStore(parsed);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
_write(value) {
|
|
701
|
+
const normalized = normalizeStore(value);
|
|
702
|
+
const serialized = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
703
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_STORE_BYTES) {
|
|
704
|
+
throw storeError(
|
|
705
|
+
"CC_RECORD_POLICY_DENIED",
|
|
706
|
+
"recorded skill store size limit exceeded",
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
const temporary = `${this.filePath}.tmp-${process.pid}-${randomUUID()}`;
|
|
710
|
+
const handle = openSync(temporary, "wx", 0o600);
|
|
711
|
+
try {
|
|
712
|
+
writeFileSync(handle, serialized, "utf8");
|
|
713
|
+
fsyncSync(handle);
|
|
714
|
+
} finally {
|
|
715
|
+
closeSync(handle);
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
renameSync(temporary, this.filePath);
|
|
719
|
+
if (this._secure)
|
|
720
|
+
ensurePrivateFile(this.filePath, { failIfUnavailable: true });
|
|
721
|
+
} catch (error) {
|
|
722
|
+
rmSync(temporary, { force: true });
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
return normalized;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
_transaction(mutator) {
|
|
729
|
+
return withFileLock(
|
|
730
|
+
this.filePath,
|
|
731
|
+
() => {
|
|
732
|
+
const current = this._read();
|
|
733
|
+
const mutable = clone(current);
|
|
734
|
+
delete mutable.storeDigest;
|
|
735
|
+
const result = mutator(mutable);
|
|
736
|
+
mutable.revision = current.revision + 1;
|
|
737
|
+
const body = storeBody(mutable);
|
|
738
|
+
const next = this._write({
|
|
739
|
+
...body,
|
|
740
|
+
storeDigest: digest(body, STORE_DIGEST_DOMAIN),
|
|
741
|
+
});
|
|
742
|
+
return { result, store: next };
|
|
743
|
+
},
|
|
744
|
+
{ failIfUnavailable: true, timeoutMs: 5_000 },
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
get(name) {
|
|
749
|
+
const entry = this._read().records[assertName(name)];
|
|
750
|
+
if (!entry)
|
|
751
|
+
throw storeError("CC_RECORD_NOT_FOUND", "recorded skill was not found");
|
|
752
|
+
return entry;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
list() {
|
|
756
|
+
return Object.values(this._read().records)
|
|
757
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
758
|
+
.map(summary);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
policy() {
|
|
762
|
+
return this._read().policy;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
setPolicy(policy, { actor = "cli" } = {}) {
|
|
766
|
+
const normalizedPolicy = normalizePolicy(policy);
|
|
767
|
+
return this._transaction((document) => {
|
|
768
|
+
if (
|
|
769
|
+
Object.keys(document.records).length > normalizedPolicy.maxRecords ||
|
|
770
|
+
Object.values(document.records).some(
|
|
771
|
+
(entry) => entry.skill.actions.length > normalizedPolicy.maxActions,
|
|
772
|
+
) ||
|
|
773
|
+
document.audit.length + 1 > normalizedPolicy.maxAuditEvents
|
|
774
|
+
) {
|
|
775
|
+
throw storeError(
|
|
776
|
+
"CC_RECORD_POLICY_DENIED",
|
|
777
|
+
"new policy is narrower than current retained data",
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
document.policy = clone(normalizedPolicy);
|
|
781
|
+
const at = this._now().toISOString();
|
|
782
|
+
appendAudit(document, {
|
|
783
|
+
action: "policy_updated",
|
|
784
|
+
entry: null,
|
|
785
|
+
name: "policy",
|
|
786
|
+
actor,
|
|
787
|
+
at,
|
|
788
|
+
state: "policy",
|
|
789
|
+
});
|
|
790
|
+
return normalizedPolicy;
|
|
791
|
+
}).result;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
create({ draft, source, actor = "cli" }) {
|
|
795
|
+
const skill = validateRecordedSkillDraft(draft);
|
|
796
|
+
const normalizedSource = normalizeSource(source);
|
|
797
|
+
assertBrowserSourceBinding(
|
|
798
|
+
skill,
|
|
799
|
+
normalizedSource,
|
|
800
|
+
"CC_RECORD_SOURCE_MISMATCH",
|
|
801
|
+
);
|
|
802
|
+
return this._transaction((document) => {
|
|
803
|
+
if (document.records[skill.name]) {
|
|
804
|
+
throw storeError(
|
|
805
|
+
"CC_RECORD_ALREADY_EXISTS",
|
|
806
|
+
"recorded skill already exists",
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if (Object.keys(document.records).length >= document.policy.maxRecords) {
|
|
810
|
+
throw storeError(
|
|
811
|
+
"CC_RECORD_POLICY_DENIED",
|
|
812
|
+
"recorded skill record limit reached",
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (
|
|
816
|
+
skill.actions.length > document.policy.maxActions ||
|
|
817
|
+
skill.capabilityManifest.some(
|
|
818
|
+
(capability) =>
|
|
819
|
+
!document.policy.allowedCapabilities.includes(capability),
|
|
820
|
+
)
|
|
821
|
+
) {
|
|
822
|
+
throw storeError(
|
|
823
|
+
"CC_RECORD_POLICY_DENIED",
|
|
824
|
+
"recorded skill is outside policy",
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
const at = this._now().toISOString();
|
|
828
|
+
const entry = sealEntry({
|
|
829
|
+
schema: ENTRY_SCHEMA,
|
|
830
|
+
name: skill.name,
|
|
831
|
+
revision: 1,
|
|
832
|
+
state: "draft",
|
|
833
|
+
createdAt: at,
|
|
834
|
+
updatedAt: at,
|
|
835
|
+
expiresAt: new Date(
|
|
836
|
+
this._now().getTime() + document.policy.retentionDays * 86_400_000,
|
|
837
|
+
).toISOString(),
|
|
838
|
+
source: normalizedSource,
|
|
839
|
+
skill,
|
|
840
|
+
lastReplay: null,
|
|
841
|
+
installation: null,
|
|
842
|
+
});
|
|
843
|
+
document.records[entry.name] = clone(entry);
|
|
844
|
+
appendAudit(document, {
|
|
845
|
+
action: "created",
|
|
846
|
+
entry,
|
|
847
|
+
name: entry.name,
|
|
848
|
+
actor,
|
|
849
|
+
at,
|
|
850
|
+
});
|
|
851
|
+
return entry;
|
|
852
|
+
}).result;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
_update(name, expectedRevision, actor, action, update) {
|
|
856
|
+
const safeName = assertName(name);
|
|
857
|
+
return this._transaction((document) => {
|
|
858
|
+
const current = document.records[safeName];
|
|
859
|
+
if (!current)
|
|
860
|
+
throw storeError("CC_RECORD_NOT_FOUND", "recorded skill was not found");
|
|
861
|
+
if (current.revision !== Number(expectedRevision)) {
|
|
862
|
+
throw storeError(
|
|
863
|
+
"CC_RECORD_REVISION_CONFLICT",
|
|
864
|
+
"recorded skill revision changed",
|
|
865
|
+
{
|
|
866
|
+
expectedRevision: Number(expectedRevision),
|
|
867
|
+
actualRevision: current.revision,
|
|
868
|
+
},
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
const at = this._now().toISOString();
|
|
872
|
+
const next = sealEntry({
|
|
873
|
+
...clone(current),
|
|
874
|
+
...update(normalizeEntry(current), document.policy, at),
|
|
875
|
+
revision: current.revision + 1,
|
|
876
|
+
updatedAt: at,
|
|
877
|
+
});
|
|
878
|
+
document.records[safeName] = clone(next);
|
|
879
|
+
appendAudit(document, { action, entry: next, name: safeName, actor, at });
|
|
880
|
+
return next;
|
|
881
|
+
}).result;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
approve({ name, expectedRevision, skill, actor }) {
|
|
885
|
+
const approved = validateReviewedRecordedSkill(skill);
|
|
886
|
+
return this._update(
|
|
887
|
+
name,
|
|
888
|
+
expectedRevision,
|
|
889
|
+
actor,
|
|
890
|
+
"approved",
|
|
891
|
+
(current) => {
|
|
892
|
+
if (
|
|
893
|
+
current.state !== "draft" ||
|
|
894
|
+
current.skill.draftDigest !== approved.draftDigest
|
|
895
|
+
) {
|
|
896
|
+
throw storeError(
|
|
897
|
+
"CC_RECORD_STATE_CONFLICT",
|
|
898
|
+
"approval does not match the current draft",
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
state: "approved",
|
|
903
|
+
skill: approved,
|
|
904
|
+
lastReplay: null,
|
|
905
|
+
installation: null,
|
|
906
|
+
};
|
|
907
|
+
},
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
recordReplay({
|
|
912
|
+
name,
|
|
913
|
+
expectedRevision,
|
|
914
|
+
report,
|
|
915
|
+
targetDigest,
|
|
916
|
+
browserVersion,
|
|
917
|
+
durationMs,
|
|
918
|
+
actor = "cli",
|
|
919
|
+
}) {
|
|
920
|
+
return this._update(
|
|
921
|
+
name,
|
|
922
|
+
expectedRevision,
|
|
923
|
+
actor,
|
|
924
|
+
"replayed",
|
|
925
|
+
(current, _policy, at) => {
|
|
926
|
+
if (
|
|
927
|
+
!["approved", "validated", "enabled", "revoked"].includes(
|
|
928
|
+
current.state,
|
|
929
|
+
)
|
|
930
|
+
) {
|
|
931
|
+
throw storeError(
|
|
932
|
+
"CC_RECORD_STATE_CONFLICT",
|
|
933
|
+
"recorded skill must be approved before replay evidence is stored",
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const verifiedReport = validateRecordedSkillReplayReport(report, {
|
|
937
|
+
skill: current.skill,
|
|
938
|
+
});
|
|
939
|
+
if (targetDigest !== current.source.targetDigest) {
|
|
940
|
+
throw storeError(
|
|
941
|
+
"CC_RECORD_TARGET_DRIFT",
|
|
942
|
+
"replay target does not match the recording",
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
state: current.state === "enabled" ? "enabled" : "validated",
|
|
947
|
+
lastReplay: {
|
|
948
|
+
report: verifiedReport,
|
|
949
|
+
targetDigest,
|
|
950
|
+
browserVersion: String(browserVersion || "unknown").slice(0, 128),
|
|
951
|
+
durationMs: Number(durationMs),
|
|
952
|
+
at,
|
|
953
|
+
},
|
|
954
|
+
installation:
|
|
955
|
+
current.state === "enabled" ? current.installation : null,
|
|
956
|
+
};
|
|
957
|
+
},
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
enable({ name, expectedRevision, scope, packageDigest, actor = "cli" }) {
|
|
962
|
+
return this._update(
|
|
963
|
+
name,
|
|
964
|
+
expectedRevision,
|
|
965
|
+
actor,
|
|
966
|
+
"enabled",
|
|
967
|
+
(current, policy, at) => {
|
|
968
|
+
if (current.state !== "validated") {
|
|
969
|
+
throw storeError(
|
|
970
|
+
"CC_RECORD_STATE_CONFLICT",
|
|
971
|
+
"recorded skill must pass replay before enablement",
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
if (scope === "global" && !policy.allowGlobalInstall) {
|
|
975
|
+
throw storeError(
|
|
976
|
+
"CC_RECORD_POLICY_DENIED",
|
|
977
|
+
"global recorded skill installation is denied by policy",
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
state: "enabled",
|
|
982
|
+
installation: { scope, packageDigest, installedAt: at },
|
|
983
|
+
};
|
|
984
|
+
},
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
revoke({ name, expectedRevision, actor = "cli" }) {
|
|
989
|
+
return this._update(name, expectedRevision, actor, "revoked", (current) => {
|
|
990
|
+
if (current.state !== "enabled") {
|
|
991
|
+
throw storeError(
|
|
992
|
+
"CC_RECORD_STATE_CONFLICT",
|
|
993
|
+
"recorded skill is not enabled",
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
return { state: "revoked", installation: null };
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
delete({ name, expectedRevision, actor = "cli" }) {
|
|
1001
|
+
const safeName = assertName(name);
|
|
1002
|
+
return this._transaction((document) => {
|
|
1003
|
+
const current = document.records[safeName];
|
|
1004
|
+
if (!current)
|
|
1005
|
+
throw storeError("CC_RECORD_NOT_FOUND", "recorded skill was not found");
|
|
1006
|
+
if (current.revision !== Number(expectedRevision)) {
|
|
1007
|
+
throw storeError(
|
|
1008
|
+
"CC_RECORD_REVISION_CONFLICT",
|
|
1009
|
+
"recorded skill revision changed",
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (current.state === "enabled") {
|
|
1013
|
+
throw storeError(
|
|
1014
|
+
"CC_RECORD_STATE_CONFLICT",
|
|
1015
|
+
"revoke the recorded skill before deleting it",
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
delete document.records[safeName];
|
|
1019
|
+
appendAudit(document, {
|
|
1020
|
+
action: "deleted",
|
|
1021
|
+
entry: null,
|
|
1022
|
+
name: safeName,
|
|
1023
|
+
actor,
|
|
1024
|
+
at: this._now().toISOString(),
|
|
1025
|
+
});
|
|
1026
|
+
return summary(current);
|
|
1027
|
+
}).result;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export(name, { actor = "cli" } = {}) {
|
|
1031
|
+
const safeName = assertName(name);
|
|
1032
|
+
return this._transaction((document) => {
|
|
1033
|
+
const entry = document.records[safeName];
|
|
1034
|
+
if (!entry)
|
|
1035
|
+
throw storeError("CC_RECORD_NOT_FOUND", "recorded skill was not found");
|
|
1036
|
+
const at = this._now().toISOString();
|
|
1037
|
+
const body = {
|
|
1038
|
+
schema: EXPORT_SCHEMA,
|
|
1039
|
+
exportedAt: at,
|
|
1040
|
+
entry: normalizeEntry(entry),
|
|
1041
|
+
};
|
|
1042
|
+
appendAudit(document, {
|
|
1043
|
+
action: "exported",
|
|
1044
|
+
entry,
|
|
1045
|
+
name: safeName,
|
|
1046
|
+
actor,
|
|
1047
|
+
at,
|
|
1048
|
+
});
|
|
1049
|
+
return deepFreeze({
|
|
1050
|
+
...clone(body),
|
|
1051
|
+
exportDigest: digest(body, EXPORT_DIGEST_DOMAIN),
|
|
1052
|
+
});
|
|
1053
|
+
}).result;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
import(value, { actor = "cli" } = {}) {
|
|
1057
|
+
assertObject(value, "CC_RECORD_IMPORT_INVALID", "recorded skill export");
|
|
1058
|
+
assertExactKeys(
|
|
1059
|
+
value,
|
|
1060
|
+
new Set(["schema", "exportedAt", "entry", "exportDigest"]),
|
|
1061
|
+
"CC_RECORD_IMPORT_INVALID",
|
|
1062
|
+
"recorded skill export",
|
|
1063
|
+
);
|
|
1064
|
+
const body = {
|
|
1065
|
+
schema: value.schema,
|
|
1066
|
+
exportedAt: assertTimestamp(value.exportedAt, "export timestamp"),
|
|
1067
|
+
entry: normalizeEntry(value.entry),
|
|
1068
|
+
};
|
|
1069
|
+
if (
|
|
1070
|
+
value.schema !== EXPORT_SCHEMA ||
|
|
1071
|
+
value.exportDigest !== digest(body, EXPORT_DIGEST_DOMAIN)
|
|
1072
|
+
) {
|
|
1073
|
+
throw storeError(
|
|
1074
|
+
"CC_RECORD_IMPORT_INVALID",
|
|
1075
|
+
"recorded skill export digest is invalid",
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
const imported = body.entry;
|
|
1079
|
+
const importedState = imported.lastReplay
|
|
1080
|
+
? "validated"
|
|
1081
|
+
: imported.state === "draft"
|
|
1082
|
+
? "draft"
|
|
1083
|
+
: "approved";
|
|
1084
|
+
const skill =
|
|
1085
|
+
importedState === "draft"
|
|
1086
|
+
? validateRecordedSkillDraft(imported.skill)
|
|
1087
|
+
: validateReviewedRecordedSkill(imported.skill);
|
|
1088
|
+
return this._transaction((document) => {
|
|
1089
|
+
if (document.records[imported.name]) {
|
|
1090
|
+
throw storeError(
|
|
1091
|
+
"CC_RECORD_ALREADY_EXISTS",
|
|
1092
|
+
"recorded skill already exists",
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
if (Object.keys(document.records).length >= document.policy.maxRecords) {
|
|
1096
|
+
throw storeError(
|
|
1097
|
+
"CC_RECORD_POLICY_DENIED",
|
|
1098
|
+
"recorded skill record limit reached",
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
const at = this._now().toISOString();
|
|
1102
|
+
const entry = sealEntry({
|
|
1103
|
+
...clone(imported),
|
|
1104
|
+
revision: 1,
|
|
1105
|
+
state: importedState,
|
|
1106
|
+
createdAt: at,
|
|
1107
|
+
updatedAt: at,
|
|
1108
|
+
expiresAt: new Date(
|
|
1109
|
+
this._now().getTime() + document.policy.retentionDays * 86_400_000,
|
|
1110
|
+
).toISOString(),
|
|
1111
|
+
skill,
|
|
1112
|
+
installation: null,
|
|
1113
|
+
});
|
|
1114
|
+
document.records[entry.name] = clone(entry);
|
|
1115
|
+
appendAudit(document, {
|
|
1116
|
+
action: "imported",
|
|
1117
|
+
entry,
|
|
1118
|
+
name: entry.name,
|
|
1119
|
+
actor,
|
|
1120
|
+
at,
|
|
1121
|
+
});
|
|
1122
|
+
return entry;
|
|
1123
|
+
}).result;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
audit({ name } = {}) {
|
|
1127
|
+
const safeName = name == null ? null : assertName(name);
|
|
1128
|
+
return this._read().audit.filter(
|
|
1129
|
+
(event) => !safeName || event.name === safeName,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
pruneExpired({ actor = "cli" } = {}) {
|
|
1134
|
+
const now = this._now();
|
|
1135
|
+
return this._transaction((document) => {
|
|
1136
|
+
const removed = [];
|
|
1137
|
+
for (const [name, entry] of Object.entries(document.records)) {
|
|
1138
|
+
if (
|
|
1139
|
+
entry.state === "enabled" ||
|
|
1140
|
+
Date.parse(entry.expiresAt) > now.getTime()
|
|
1141
|
+
)
|
|
1142
|
+
continue;
|
|
1143
|
+
delete document.records[name];
|
|
1144
|
+
removed.push(name);
|
|
1145
|
+
appendAudit(document, {
|
|
1146
|
+
action: "retention_pruned",
|
|
1147
|
+
entry: null,
|
|
1148
|
+
name,
|
|
1149
|
+
actor,
|
|
1150
|
+
at: now.toISOString(),
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
return deepFreeze(removed.sort());
|
|
1154
|
+
}).result;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
export function recordedSkillStoreSchemas() {
|
|
1159
|
+
return Object.freeze({
|
|
1160
|
+
store: STORE_SCHEMA,
|
|
1161
|
+
entry: ENTRY_SCHEMA,
|
|
1162
|
+
export: EXPORT_SCHEMA,
|
|
1163
|
+
policy: POLICY_SCHEMA,
|
|
1164
|
+
audit: AUDIT_SCHEMA,
|
|
1165
|
+
});
|
|
1166
|
+
}
|