chainlesschain 0.162.151 → 0.162.153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-DuEOzebi.js → AIOps-CBilhs1R.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C6GiJa5H.js → ActionButton-Cf-RnC5Y.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BBIiL_pO.js → Analytics-S8tkGhw0.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CuI2gZRy.js → AppLayout-CNXI3Bw5.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-CbUjfr-K.js → Audit-DF2zVFZ0.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CsSW9ljf.js → Backup-CMEiwQZL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-kM3rLe7F.js → BaseInput-rwLw4K70.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-kfE51YN3.js → Chat-C478H8u9.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DNQsKN61.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-jYb1uj-M.js → Checkbox-D-47X635.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-X9XiD5Lj.js → Codegen-Cb9Ihed5.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BfbHu1xI.js → Col-CQsdzczt.js} +1 -1
- package/src/assets/web-panel/assets/{Community-c4oJMFS-.js → Community-DiqzeAy-.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-BFmnZpH7.js → Compact-Bq-APyyF.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-s9shVHBS.js → Compliance-Dnb1YxPr.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-C5da--TC.js → Cowork-CZljerdQ.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-9kNbyNE4.js → Cron-gAdbZOq7.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-C1k3NJD4.js → Crosschain-DMPGXttD.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DkheEvNK.js → DID-C7dBDAv8.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-C3FuSGmJ.js → Dashboard-ByR4VcF8.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CAV6FMFD.js → Dropdown-Bc4tbiQN.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Cc-Ulc4l.js → EmailListRenderer-BhUN2pDw.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-8CitHEtv.js → FamilyGuardDashboard-DpJuANCD.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DFSZ5KDt.js → Federation-BEm4TvcE.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CEWCtnG2.js → FormItemContext-JnkPkhiC.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CwRzNzmX.js → GenericCardRenderer-BfCzFUIC.js} +1 -1
- package/src/assets/web-panel/assets/{Git-0rgDM7cJ.js → Git-1c0MWpmD.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CfBKtK9F.js → Governance-C_j3EBpv.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CzcUVvud.js → Inference-Cb7XRgtY.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Dwc4YL4A.js → KnowledgeGraph-DoHPvHyK.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DgFPwR2F.js → Logs-CYriKpy-.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-DnpPTbGU.js → Marketplace-BT56-GD0.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DRxxINyW.js → McpTools-B6XX_wnZ.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-OhmW_6Z3.js → Memory-DRgNuaQc.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DkTW-RK5.js → MobileBridge-B0qgvBN2.js} +1 -1
- package/src/assets/web-panel/assets/MobileProjects-CRRfswf7.js +1 -0
- package/src/assets/web-panel/assets/{Mtc--vmkL0AS.js → Mtc-DnSFj9jw.js} +4 -4
- package/src/assets/web-panel/assets/{MtcAudit-DGoFWjD4.js → MtcAudit-c0tuz1sm.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-B4kWgq95.js → Multisig-CVLBGioX.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-YQwvommE.js → NLProgramming-CFaqmNbx.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CgjtUADJ.js → Notes-BM_mNwU_.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-6JDrpBG5.js → NotificationSettings-Sa1k3ReS.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-lWzmtqBh.js +1 -0
- package/src/assets/web-panel/assets/{Organization-8McOT_OF.js → Organization-C2blcq-P.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DAZpSCGc.js → Overflow-B3PwOJnK.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-B7l2ZWLy.js → P2P-DaB9T6tb.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-W1Ud-NAL.js → PdhVaultBrowser-BpEBZ72n.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-Czm_w9u_.js → Permissions-BTkWInyH.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-B4F0_ZVM.js → PersonalDataHub-LHEwNmyL.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-B37aFiWv.js → Pipeline-DvWIxqH-.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-94KBnRrD.js → Privacy-BDbpBagU.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-B5h8_dlh.js → ProjectInit-DLeMDWWv.js} +2 -2
- package/src/assets/web-panel/assets/ProjectSettings-BK3NsxIz.js +2 -0
- package/src/assets/web-panel/assets/Projects-BPWPg9zA.js +1 -0
- package/src/assets/web-panel/assets/{Providers-B3cNWwJg.js → Providers-D2EvmPAd.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CHRvOw1R.js → QrScannerModal-DAcu2fNg.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D1JMgFEd.js → QuickAsk-B7U850Yt.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-EYWGR79p.js → Recommend-bxNXLzOR.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-BAMC2pJt.js → RemoteSession-BsCjAfSJ.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-X2Hk1XG_.js → Reputation-DZ9LPu-R.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Eq98LRtU.js → Row-cIYkCB3Y.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-i76s6z_o.js → RssFeed-BxTNTtTp.js} +2 -2
- package/src/assets/web-panel/assets/{Search-D_Es7CAI.js → Search-DSar6GhE.js} +1 -1
- package/src/assets/web-panel/assets/{Security-kayD2e94.js → Security-oCbIGysL.js} +4 -4
- package/src/assets/web-panel/assets/{Services-Ctsm6ul8.js → Services-D8yojrNv.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CRNYlfdf.js → Skeleton-vTA6P1eL.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DkZYpF4-.js → Skills-BXLf_KgZ.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DXkoBcOP.js → Sla-Cpwxzz__.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DXi_GI1d.js → SpeechSettings-DcPN9kCb.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-rNKpfomn.js → SyncSettings-DY0U5Vuj.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-XF4uPtG-.js → Tasks-DKu3-sZJ.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-i0Hzfeyu.js → Templates-MD3nfWGm.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-sMpHAi27.js → Tenant-DKVnyGXM.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-DQMIZfWf.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-yuC6K7Rf.js → TimelineRenderer-CugmNZTs.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-B8O8i0s6.js → Tokens-Cjvlttu6.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-Bv6yrlCr.js → Trigger-CzNOEcoI.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-C4mW95Ev.js → Trust-D5jg9iPA.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DV6yKaHn.js → UkeySign-59bMlvxY.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-UI479-sD.js → VideoEditing-DmCzSBqn.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-M5oV9EVP.js → Wallet-DoFe8V6W.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DDyDHPDR.js → WebAuthn-9POLER-w.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-RoBv4rqk.js → WorkflowEditor-cFQH0Og6.js} +1 -1
- package/src/assets/web-panel/assets/{chat-Cw1mE5LS.js → chat-D3fR2NkZ.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BFP62Gwf.js → colors-DiLEwE_P.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-17HL6pyC.js → compact-item-CGfFDdic.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D55X4fZX.js → createContext-BbIbOPL2.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-B6FRgsmP.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-D3E5mNsP.js → hasIn-DqyWp63_.js} +1 -1
- package/src/assets/web-panel/assets/{index-DOjA8bvj.js → index-BCoDp6o_.js} +1 -1
- package/src/assets/web-panel/assets/{index-tVRPgYGr.js → index-BKeRBUtO.js} +1 -1
- package/src/assets/web-panel/assets/{index-RYZgFIwo.js → index-BKxvHCqz.js} +1 -1
- package/src/assets/web-panel/assets/{index-Da27u6j0.js → index-BiLP6SNu.js} +1 -1
- package/src/assets/web-panel/assets/{index-CFYGn88j.js → index-BtcjYHgn.js} +1 -1
- package/src/assets/web-panel/assets/{index-paVubXzn.js → index-BtwvOuvO.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4LHSde4.js → index-ByE5tKjP.js} +1 -1
- package/src/assets/web-panel/assets/{index-C_MfcrLf.js → index-C1S30j-Z.js} +1 -1
- package/src/assets/web-panel/assets/{index-DkBVYlE4.js → index-C2lZXIEA.js} +1 -1
- package/src/assets/web-panel/assets/{index-ITeEHb8L.js → index-CH8gSMSA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BC6zxOlU.js → index-CHpI6g_S.js} +1 -1
- package/src/assets/web-panel/assets/{index-KtLJXn4s.js → index-CIleIMTT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BY_xZ2jy.js → index-CToOzfR2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bzpp2z2S.js → index-CZQshOyI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvCFKojj.js → index-ComvO4hy.js} +1 -1
- package/src/assets/web-panel/assets/index-Cq411qtc.js +1 -0
- package/src/assets/web-panel/assets/{index-tUH_6bdJ.js → index-CweXQM71.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cdvk2_2q.js → index-CySlHabI.js} +1 -1
- package/src/assets/web-panel/assets/{index-gAVxDiTZ.js → index-D16FSRMW.js} +1 -1
- package/src/assets/web-panel/assets/index-D1oX0DK3.js +1 -0
- package/src/assets/web-panel/assets/{index-CLEzsZ8A.js → index-D3vQv3Kq.js} +1 -1
- package/src/assets/web-panel/assets/{index-BEa1RW4c.js → index-DH6qaU1a.js} +1 -1
- package/src/assets/web-panel/assets/{index-BQmZg5si.js → index-DPxmTldu.js} +1 -1
- package/src/assets/web-panel/assets/{index-ASPYTzMK.js → index-DQH--Uw5.js} +1 -1
- package/src/assets/web-panel/assets/{index-uSsiwgZ2.js → index-Db9UJVjj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CR9rNUvi.js → index-DdLwxj-F.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bq83UBDr.js → index-DdjXemK-.js} +1 -1
- package/src/assets/web-panel/assets/{index-H9_X1Cg_.js → index-DzuGkFvy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DD_qGBrt.js → index-FFlp8Jx9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnGpft-Z.js → index-Km36XQ3z.js} +1 -1
- package/src/assets/web-panel/assets/{index-CjCwi7we.js → index-auI9fxYH.js} +1 -1
- package/src/assets/web-panel/assets/{index-DzG7mkgW.js → index-fCfaVIr7.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtAnPBWP.js → index-hVGbmc7y.js} +1 -1
- package/src/assets/web-panel/assets/{index-DCLu84x-.js → index-kL8f4MRS.js} +1 -1
- package/src/assets/web-panel/assets/{index--44J50mc.js → index-ns6bNnGo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CW6LLoJO.js → index-qHvMk0U3.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIRvdwBX.js → index-rTrejDjk.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGyTos4p.js → index-tLaW5UzZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BgN5G5vF.js → index-xm1KHUEQ.js} +27 -27
- package/src/assets/web-panel/assets/{initDefaultProps-Ckp7xVZS.js → initDefaultProps-pg_7pnd6.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DUZgCpn6.js → motion-B5GoJSZL.js} +1 -1
- package/src/assets/web-panel/assets/{move-B2rttglH.js → move-BaJoe2FW.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BYWcLTuN.js → mtc-parser-CiQGbqtr.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DhHqKeFx.js → omit-BFNEsKvU.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-ANpJaWMO.js → pickAttrs-YbSoANHm.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CfZWe7CA.js → placementArrow-C40MJPIH.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-D7Xz2y-R.js → responsiveObserve-LaVz2zF6.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DekqCBY9.js → slide-BkSoAGxF.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BET6XU7h.js → statusUtils-C4jWjohA.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-qHmJjV62.js → styleChecker-BGqeIh-m.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-BoMCTNyu.js → useFlexGapSupport-JsR5NMrM.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-TdW-NgFC.js → useFs-DOiP38RR.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Dyox7wCu.js → usePersonalDataHub-C-nC0ymt.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-XUn5S8CP.js → vnode-BalilHOU.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Ls77i8hn.js → zoom-DjUELqks.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +8 -1
- package/src/commands/eval.js +88 -23
- package/src/commands/plugin.js +10 -0
- package/src/commands/team.js +60 -3
- package/src/data/changelog.json +17 -2
- package/src/harness/mcp-client.js +115 -1
- package/src/harness/remote-command-ledger.js +60 -10
- package/src/lib/agent-sandbox.js +11 -5
- package/src/lib/agent-team/task-lease.js +28 -0
- package/src/lib/agent-team/team-runner.js +45 -0
- package/src/lib/config-manager.js +14 -1
- package/src/lib/eval/tasks.js +62 -41
- package/src/lib/eval/trend.js +7 -1
- package/src/lib/lsp/__tests__/lsp-client.test.js +12 -6
- package/src/lib/lsp/code-intelligence.js +55 -17
- package/src/lib/lsp/lsp-client.js +7 -1
- package/src/lib/plugin-runtime/install.js +20 -1
- package/src/lib/plugin-runtime/policy.js +13 -0
- package/src/lib/plugin-runtime/remote-source.js +34 -0
- package/src/lib/plugin-runtime/signature.js +79 -22
- package/src/lib/plugin-security.js +13 -14
- package/src/lib/sub-agent-context.js +28 -7
- package/src/lib/telemetry/span-recorder.js +30 -7
- package/src/repl/agent-repl.js +64 -21
- package/src/repl/permission-prompt.js +58 -0
- package/src/runtime/__tests__/auto-pin.test.js +27 -4
- package/src/runtime/agent-core.js +236 -1
- package/src/runtime/auto-pin.js +19 -0
- package/src/runtime/coding-agent-contract-shared.cjs +5 -0
- package/src/runtime/headless-runner.js +23 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-Bg2FTW6A.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-DpCGV_lI.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-PM6IlxjO.js +0 -1
- package/src/assets/web-panel/assets/ProjectSettings-lmeI3Lpm.js +0 -2
- package/src/assets/web-panel/assets/Projects-D5AYOasl.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BoZvaYAl.js +0 -3
- package/src/assets/web-panel/assets/devWarning-CqbHYuRc.js +0 -1
- package/src/assets/web-panel/assets/index-CKBI1U5K.js +0 -1
- package/src/assets/web-panel/assets/index-CkLp6DTO.js +0 -1
package/src/lib/eval/tasks.js
CHANGED
|
@@ -30,6 +30,39 @@ const STRINGUTIL_V2 =
|
|
|
30
30
|
" return str.charAt(0).toUpperCase() + str.slice(1);\n" +
|
|
31
31
|
"}\n";
|
|
32
32
|
|
|
33
|
+
// Harness files the agent is told NOT to edit, shared by setup and check so the
|
|
34
|
+
// check can byte-compare them (same anti-cheat as STRINGUTIL_V2 above). A mere
|
|
35
|
+
// presence-regex is neuterable: prepending `console.log('ALL OK');
|
|
36
|
+
// process.exit(0)` keeps the assertion text present while never running it.
|
|
37
|
+
const RUN_CHECKS_MJS = [
|
|
38
|
+
"import { sum } from './calc.js';",
|
|
39
|
+
"const cases = [[1,2,3],[10,5,15],[0,0,0]];",
|
|
40
|
+
"for (const [a,b,want] of cases) {",
|
|
41
|
+
" if (sum(a,b) !== want) {",
|
|
42
|
+
" console.error(`FAIL sum(${a},${b})=${sum(a,b)} want ${want}`);",
|
|
43
|
+
" process.exit(1);",
|
|
44
|
+
" }",
|
|
45
|
+
"}",
|
|
46
|
+
"console.log('ALL OK');",
|
|
47
|
+
"",
|
|
48
|
+
].join("\n");
|
|
49
|
+
|
|
50
|
+
const BUILD_MJS =
|
|
51
|
+
'import { banner } from "./app.mjs";\n' +
|
|
52
|
+
'if (banner() !== "v1.0.0") {\n' +
|
|
53
|
+
' console.error("BUILD FAIL: " + banner());\n' +
|
|
54
|
+
" process.exit(1);\n" +
|
|
55
|
+
"}\n" +
|
|
56
|
+
'console.log("BUILD OK");\n';
|
|
57
|
+
|
|
58
|
+
const RUN_MJS =
|
|
59
|
+
'import { label } from "./app.mjs";\n' +
|
|
60
|
+
'if (label("hello") !== "Hello") {\n' +
|
|
61
|
+
' console.error("FAIL: " + label("hello"));\n' +
|
|
62
|
+
" process.exit(1);\n" +
|
|
63
|
+
"}\n" +
|
|
64
|
+
'console.log("OK");\n';
|
|
65
|
+
|
|
33
66
|
export const BUILTIN_TASKS = [
|
|
34
67
|
{
|
|
35
68
|
id: "create-file",
|
|
@@ -134,32 +167,22 @@ export const BUILTIN_TASKS = [
|
|
|
134
167
|
);
|
|
135
168
|
fs.writeFileSync(
|
|
136
169
|
path.join(dir, "run-checks.mjs"),
|
|
137
|
-
|
|
138
|
-
"import { sum } from './calc.js';",
|
|
139
|
-
"const cases = [[1,2,3],[10,5,15],[0,0,0]];",
|
|
140
|
-
"for (const [a,b,want] of cases) {",
|
|
141
|
-
" if (sum(a,b) !== want) {",
|
|
142
|
-
" console.error(`FAIL sum(${a},${b})=${sum(a,b)} want ${want}`);",
|
|
143
|
-
" process.exit(1);",
|
|
144
|
-
" }",
|
|
145
|
-
"}",
|
|
146
|
-
"console.log('ALL OK');",
|
|
147
|
-
"",
|
|
148
|
-
].join("\n"),
|
|
170
|
+
RUN_CHECKS_MJS,
|
|
149
171
|
"utf8",
|
|
150
172
|
);
|
|
151
173
|
},
|
|
152
174
|
check: (dir) => {
|
|
153
|
-
// Objective: the test harness must actually pass.
|
|
154
|
-
//
|
|
155
|
-
//
|
|
175
|
+
// Objective: the test harness must actually pass. Byte-compare the
|
|
176
|
+
// harness — a presence-regex alone is neuterable (prepend an early
|
|
177
|
+
// `console.log('ALL OK'); process.exit(0)` and the assertion text is
|
|
178
|
+
// still there but never runs).
|
|
156
179
|
const checks = read(dir, "run-checks.mjs");
|
|
157
180
|
if (checks == null)
|
|
158
181
|
return { pass: false, detail: "run-checks.mjs missing" };
|
|
159
|
-
if (
|
|
182
|
+
if (checks !== RUN_CHECKS_MJS) {
|
|
160
183
|
return {
|
|
161
184
|
pass: false,
|
|
162
|
-
detail: "run-checks.mjs
|
|
185
|
+
detail: "run-checks.mjs was edited (fix calc.js instead)",
|
|
163
186
|
};
|
|
164
187
|
}
|
|
165
188
|
try {
|
|
@@ -487,23 +510,19 @@ export const BUILTIN_TASKS = [
|
|
|
487
510
|
'export function banner() {\n return "v" + VERSION;\n}\n',
|
|
488
511
|
"utf8",
|
|
489
512
|
);
|
|
490
|
-
fs.writeFileSync(
|
|
491
|
-
path.join(dir, "build.mjs"),
|
|
492
|
-
'import { banner } from "./app.mjs";\n' +
|
|
493
|
-
'if (banner() !== "v1.0.0") {\n' +
|
|
494
|
-
' console.error("BUILD FAIL: " + banner());\n' +
|
|
495
|
-
" process.exit(1);\n" +
|
|
496
|
-
"}\n" +
|
|
497
|
-
'console.log("BUILD OK");\n',
|
|
498
|
-
"utf8",
|
|
499
|
-
);
|
|
513
|
+
fs.writeFileSync(path.join(dir, "build.mjs"), BUILD_MJS, "utf8");
|
|
500
514
|
},
|
|
501
515
|
check: (dir) => {
|
|
502
516
|
const build = read(dir, "build.mjs");
|
|
503
517
|
if (build == null) return { pass: false, detail: "build.mjs missing" };
|
|
504
|
-
// Guard: fix the modules, don't neuter the build harness.
|
|
505
|
-
|
|
506
|
-
|
|
518
|
+
// Guard: fix the modules, don't neuter the build harness. Byte-compare —
|
|
519
|
+
// a presence-regex is satisfiable by a rewritten build.mjs that defines
|
|
520
|
+
// its own banner() and never imports app.mjs.
|
|
521
|
+
if (build !== BUILD_MJS) {
|
|
522
|
+
return {
|
|
523
|
+
pass: false,
|
|
524
|
+
detail: "build.mjs was edited (reconcile config.mjs/app.mjs instead)",
|
|
525
|
+
};
|
|
507
526
|
}
|
|
508
527
|
try {
|
|
509
528
|
const out = execFileSync(process.execPath, ["build.mjs"], {
|
|
@@ -534,7 +553,7 @@ export const BUILTIN_TASKS = [
|
|
|
534
553
|
"`capitalize` export was renamed to `toTitle` (same behavior). Do all of: " +
|
|
535
554
|
"(1) bump stringutil to ^2.0.0 in package.json; (2) update app.mjs to " +
|
|
536
555
|
"import and use `toTitle` instead of `capitalize`; (3) do NOT edit " +
|
|
537
|
-
"stringutil.mjs. Afterwards `node run.mjs` must print OK.",
|
|
556
|
+
"stringutil.mjs or run.mjs. Afterwards `node run.mjs` must print OK.",
|
|
538
557
|
setup: (dir) => {
|
|
539
558
|
fs.writeFileSync(
|
|
540
559
|
path.join(dir, "package.json"),
|
|
@@ -558,16 +577,7 @@ export const BUILTIN_TASKS = [
|
|
|
558
577
|
"export function label(s) {\n return capitalize(s);\n}\n",
|
|
559
578
|
"utf8",
|
|
560
579
|
);
|
|
561
|
-
fs.writeFileSync(
|
|
562
|
-
path.join(dir, "run.mjs"),
|
|
563
|
-
'import { label } from "./app.mjs";\n' +
|
|
564
|
-
'if (label("hello") !== "Hello") {\n' +
|
|
565
|
-
' console.error("FAIL: " + label("hello"));\n' +
|
|
566
|
-
" process.exit(1);\n" +
|
|
567
|
-
"}\n" +
|
|
568
|
-
'console.log("OK");\n',
|
|
569
|
-
"utf8",
|
|
570
|
-
);
|
|
580
|
+
fs.writeFileSync(path.join(dir, "run.mjs"), RUN_MJS, "utf8");
|
|
571
581
|
},
|
|
572
582
|
check: (dir) => {
|
|
573
583
|
// The dependency is vendored — the agent must adapt the caller, NOT edit
|
|
@@ -580,6 +590,17 @@ export const BUILTIN_TASKS = [
|
|
|
580
590
|
detail: "the stringutil dependency was edited (adapt the caller)",
|
|
581
591
|
};
|
|
582
592
|
}
|
|
593
|
+
// The run harness is equally off-limits: without this guard an agent
|
|
594
|
+
// could overwrite run.mjs with `console.log("OK")` and pass the textual
|
|
595
|
+
// checks below without app.mjs ever being executed.
|
|
596
|
+
const runner = read(dir, "run.mjs");
|
|
597
|
+
if (runner == null) return { pass: false, detail: "run.mjs missing" };
|
|
598
|
+
if (runner !== RUN_MJS) {
|
|
599
|
+
return {
|
|
600
|
+
pass: false,
|
|
601
|
+
detail: "run.mjs was edited (adapt app.mjs/package.json instead)",
|
|
602
|
+
};
|
|
603
|
+
}
|
|
583
604
|
const pkg = read(dir, "package.json");
|
|
584
605
|
if (pkg == null || !/"stringutil"\s*:\s*"[\^~]?2\./.test(pkg)) {
|
|
585
606
|
return {
|
package/src/lib/eval/trend.js
CHANGED
|
@@ -84,7 +84,13 @@ function ordered(records) {
|
|
|
84
84
|
* }}
|
|
85
85
|
*/
|
|
86
86
|
export function computeTrend(runs, { regressionThreshold = 0 } = {}) {
|
|
87
|
-
|
|
87
|
+
// A dry-run record is always 0% (the no-op agent creates nothing) — folding
|
|
88
|
+
// it in would flag every previously-passing task as a regression (or, in the
|
|
89
|
+
// other direction, fake an "all fixed" jump). Smoke-testing with
|
|
90
|
+
// `--dry-run --history` must not poison the release gate.
|
|
91
|
+
const norm = ordered(
|
|
92
|
+
(runs || []).filter((r) => r?.dryRun !== true).map((r) => summarizeRun(r)),
|
|
93
|
+
);
|
|
88
94
|
const history = norm.map((r) => r.passRate);
|
|
89
95
|
if (norm.length === 0) {
|
|
90
96
|
return {
|
|
@@ -225,14 +225,20 @@ describe("uri <-> path", () => {
|
|
|
225
225
|
describe("normalizeUri", () => {
|
|
226
226
|
it("lowercases the windows drive letter so client/server keys match", () => {
|
|
227
227
|
// path.resolve yields C:, servers echo c: — must collapse to one key
|
|
228
|
-
expect(normalizeUri("file:///C%3A/proj/a.ts")).toBe(
|
|
229
|
-
|
|
230
|
-
);
|
|
231
|
-
expect(normalizeUri("file:///c%3A/proj/a.ts")).toBe(
|
|
232
|
-
"file:///c%3A/proj/a.ts",
|
|
233
|
-
);
|
|
228
|
+
expect(normalizeUri("file:///C%3A/proj/a.ts")).toBe("file:///c:/proj/a.ts");
|
|
229
|
+
expect(normalizeUri("file:///c%3A/proj/a.ts")).toBe("file:///c:/proj/a.ts");
|
|
234
230
|
expect(normalizeUri("file:///C:/proj/a.ts")).toBe("file:///c:/proj/a.ts");
|
|
235
231
|
});
|
|
232
|
+
it("collapses the %3A and bare-colon drive forms to ONE key (gopls)", () => {
|
|
233
|
+
// The client sends percent-encoded didOpen URIs (file:///C%3A/…);
|
|
234
|
+
// tsserver/pyright echo that form back, but gopls REWRITES to a bare
|
|
235
|
+
// colon (file:///C:/…). Before this canonicalization the two forms were
|
|
236
|
+
// distinct diagnostics-Map keys, so every gopls publish missed the
|
|
237
|
+
// didOpen key and `cc code-intel diag` on Go was permanently empty.
|
|
238
|
+
const ours = normalizeUri("file:///C%3A/Users/u/goproj/broken.go");
|
|
239
|
+
const goplsPublished = normalizeUri("file:///C:/Users/u/goproj/broken.go");
|
|
240
|
+
expect(ours).toBe(goplsPublished);
|
|
241
|
+
});
|
|
236
242
|
it("leaves POSIX file URIs unchanged", () => {
|
|
237
243
|
expect(normalizeUri("file:///home/u/a.ts")).toBe("file:///home/u/a.ts");
|
|
238
244
|
});
|
|
@@ -85,11 +85,15 @@ export class CodeIntelligence {
|
|
|
85
85
|
async references(filePath, line, col, { includeDeclaration = true } = {}) {
|
|
86
86
|
const ready = await this._ensure(filePath);
|
|
87
87
|
if (ready.unavailable) return unavailable(ready.reason);
|
|
88
|
-
const result = await
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
const result = await this._readRequest(
|
|
89
|
+
ready.client,
|
|
90
|
+
"textDocument/references",
|
|
91
|
+
{
|
|
92
|
+
textDocument: { uri: ready.uri },
|
|
93
|
+
position: toLspPosition({ line, col }),
|
|
94
|
+
context: { includeDeclaration },
|
|
95
|
+
},
|
|
96
|
+
);
|
|
93
97
|
return { available: true, locations: this._normalizeLocations(result) };
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -97,7 +101,7 @@ export class CodeIntelligence {
|
|
|
97
101
|
async hover(filePath, line, col) {
|
|
98
102
|
const ready = await this._ensure(filePath);
|
|
99
103
|
if (ready.unavailable) return unavailable(ready.reason);
|
|
100
|
-
const result = await ready.client
|
|
104
|
+
const result = await this._readRequest(ready.client, "textDocument/hover", {
|
|
101
105
|
textDocument: { uri: ready.uri },
|
|
102
106
|
position: toLspPosition({ line, col }),
|
|
103
107
|
});
|
|
@@ -108,9 +112,13 @@ export class CodeIntelligence {
|
|
|
108
112
|
async documentSymbols(filePath) {
|
|
109
113
|
const ready = await this._ensure(filePath);
|
|
110
114
|
if (ready.unavailable) return unavailable(ready.reason);
|
|
111
|
-
const result = await
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
const result = await this._readRequest(
|
|
116
|
+
ready.client,
|
|
117
|
+
"textDocument/documentSymbol",
|
|
118
|
+
{
|
|
119
|
+
textDocument: { uri: ready.uri },
|
|
120
|
+
},
|
|
121
|
+
);
|
|
114
122
|
return {
|
|
115
123
|
available: true,
|
|
116
124
|
symbols: this._normalizeSymbols(result, ready.uri),
|
|
@@ -124,7 +132,9 @@ export class CodeIntelligence {
|
|
|
124
132
|
if (!seed) return unavailable("no indexable file found to start a server");
|
|
125
133
|
const ready = await this.manager.ensureFor(seed, this._ensureOpts);
|
|
126
134
|
if (ready.unavailable) return unavailable(ready.reason);
|
|
127
|
-
const result = await ready.client
|
|
135
|
+
const result = await this._readRequest(ready.client, "workspace/symbol", {
|
|
136
|
+
query,
|
|
137
|
+
});
|
|
128
138
|
return { available: true, symbols: this._normalizeSymbols(result) };
|
|
129
139
|
}
|
|
130
140
|
|
|
@@ -160,7 +170,8 @@ export class CodeIntelligence {
|
|
|
160
170
|
const caps = ready.client.serverCapabilities || {};
|
|
161
171
|
if (caps.diagnosticProvider) {
|
|
162
172
|
try {
|
|
163
|
-
const report = await
|
|
173
|
+
const report = await this._readRequest(
|
|
174
|
+
ready.client,
|
|
164
175
|
"textDocument/diagnostic",
|
|
165
176
|
{ textDocument: { uri: ready.uri } },
|
|
166
177
|
{ timeoutMs },
|
|
@@ -182,11 +193,15 @@ export class CodeIntelligence {
|
|
|
182
193
|
async renamePreview(filePath, line, col, newName) {
|
|
183
194
|
const ready = await this._ensure(filePath);
|
|
184
195
|
if (ready.unavailable) return unavailable(ready.reason);
|
|
185
|
-
const result = await
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
const result = await this._readRequest(
|
|
197
|
+
ready.client,
|
|
198
|
+
"textDocument/rename",
|
|
199
|
+
{
|
|
200
|
+
textDocument: { uri: ready.uri },
|
|
201
|
+
position: toLspPosition({ line, col }),
|
|
202
|
+
newName,
|
|
203
|
+
},
|
|
204
|
+
);
|
|
190
205
|
return { available: true, edits: this._normalizeWorkspaceEdit(result) };
|
|
191
206
|
}
|
|
192
207
|
|
|
@@ -196,10 +211,33 @@ export class CodeIntelligence {
|
|
|
196
211
|
|
|
197
212
|
// ---- internals ----
|
|
198
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Issue an idempotent READ request with a bounded ContentModified retry.
|
|
216
|
+
* LSP -32801 (ContentModified) means "server state changed mid-request —
|
|
217
|
+
* the result would be stale, ask again"; rust-analyzer emits it freely
|
|
218
|
+
* while its index is still settling after didOpen. Read-only queries can
|
|
219
|
+
* always be retried; mutating requests must NOT go through here.
|
|
220
|
+
*/
|
|
221
|
+
async _readRequest(client, method, params, opts) {
|
|
222
|
+
let lastErr;
|
|
223
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
224
|
+
try {
|
|
225
|
+
return await client.request(method, params, opts);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const contentModified =
|
|
228
|
+
err?.code === -32801 || /content modified/i.test(err?.message || "");
|
|
229
|
+
if (!contentModified) throw err;
|
|
230
|
+
lastErr = err;
|
|
231
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw lastErr;
|
|
235
|
+
}
|
|
236
|
+
|
|
199
237
|
async _locationQuery(method, filePath, line, col) {
|
|
200
238
|
const ready = await this._ensure(filePath);
|
|
201
239
|
if (ready.unavailable) return unavailable(ready.reason);
|
|
202
|
-
const result = await ready.client
|
|
240
|
+
const result = await this._readRequest(ready.client, method, {
|
|
203
241
|
textDocument: { uri: ready.uri },
|
|
204
242
|
position: toLspPosition({ line, col }),
|
|
205
243
|
});
|
|
@@ -297,9 +297,15 @@ export function pathToFileUri(p) {
|
|
|
297
297
|
*/
|
|
298
298
|
export function normalizeUri(uri) {
|
|
299
299
|
if (!uri) return uri;
|
|
300
|
+
// Canonicalize BOTH the drive-letter case AND the colon encoding. Servers
|
|
301
|
+
// differ in the form they publish back: tsserver/pyright echo the client's
|
|
302
|
+
// percent-encoded `c%3A/…`, but gopls rewrites to a bare `C:/…` — keeping
|
|
303
|
+
// the incoming colon form made the two forms distinct Map keys, so every
|
|
304
|
+
// gopls publishDiagnostics missed the didOpen key and Go diagnostics were
|
|
305
|
+
// permanently empty regardless of timeout.
|
|
300
306
|
return String(uri).replace(
|
|
301
307
|
/^(file:\/\/\/)([A-Za-z])(%3[Aa]|:)/,
|
|
302
|
-
(_m, p, drive
|
|
308
|
+
(_m, p, drive) => p + drive.toLowerCase() + ":",
|
|
303
309
|
);
|
|
304
310
|
}
|
|
305
311
|
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
discoverPlugins,
|
|
30
30
|
} from "./scopes.js";
|
|
31
31
|
import { verifyPluginManifest } from "../plugin-security.js";
|
|
32
|
-
import { writePluginLock } from "./signature.js";
|
|
32
|
+
import { writePluginLock, LOCK_FILENAME } from "./signature.js";
|
|
33
33
|
|
|
34
34
|
export const _deps = {
|
|
35
35
|
existsSync: fs.existsSync,
|
|
@@ -97,6 +97,12 @@ export function installFromDirectory(srcDir, opts = {}) {
|
|
|
97
97
|
_deps.mkdirSync(dest, { recursive: true });
|
|
98
98
|
copyDirGuarded(src, dest, dest);
|
|
99
99
|
|
|
100
|
+
// A lock may ONLY exist if THIS installer wrote it. The source is untrusted
|
|
101
|
+
// and may ship its own `.plugin-lock.json` (copied verbatim above) — on an
|
|
102
|
+
// unsigned install that forged lock would otherwise survive into the
|
|
103
|
+
// "immutable" version dir and defeat load-time requireSignedPlugins.
|
|
104
|
+
_deps.rmSync(path.join(dest, LOCK_FILENAME), { force: true });
|
|
105
|
+
|
|
100
106
|
// Record the verified signature into the installed (immutable) version dir so
|
|
101
107
|
// load-time enforcement can re-check it. The manifest was copied verbatim, so
|
|
102
108
|
// its bytes/sha in `dest` match what we verified in `src`.
|
|
@@ -110,6 +116,8 @@ export function installFromDirectory(srcDir, opts = {}) {
|
|
|
110
116
|
sha256: verification.sha256,
|
|
111
117
|
publicKeySha256: verification.publicKeySha256,
|
|
112
118
|
signatureVerified: verification.signatureVerified === true,
|
|
119
|
+
signatureBase64: verification.signatureBase64,
|
|
120
|
+
publicKeyPem: verification.publicKeyPem,
|
|
113
121
|
});
|
|
114
122
|
}
|
|
115
123
|
|
|
@@ -245,6 +253,17 @@ export function parseGitSource(raw) {
|
|
|
245
253
|
* command line — no injection). Caller removes the temp dir's parent.
|
|
246
254
|
*/
|
|
247
255
|
export function fetchGitRepo(url, ref) {
|
|
256
|
+
// git argv-injection guard: a value starting with "-" is parsed by git as an
|
|
257
|
+
// OPTION, not a URL/ref — e.g. a registry-supplied ref "-f" reaches
|
|
258
|
+
// `git checkout <ref>` on the full-clone retry path, and an option-looking
|
|
259
|
+
// url reaches `git clone`. Real git URLs/refs never start with "-"
|
|
260
|
+
// (check-ref-format forbids it), so reject instead of trying to escape.
|
|
261
|
+
if (String(url).startsWith("-")) {
|
|
262
|
+
throw new Error(`refusing git source that looks like an option: ${url}`);
|
|
263
|
+
}
|
|
264
|
+
if (ref != null && String(ref).startsWith("-")) {
|
|
265
|
+
throw new Error(`refusing git ref that looks like an option: ${ref}`);
|
|
266
|
+
}
|
|
248
267
|
const base = _deps.mkdtempSync(path.join(os.tmpdir(), "cc-plugin-git-"));
|
|
249
268
|
const dir = path.join(base, "repo");
|
|
250
269
|
const run = (args) =>
|
|
@@ -115,6 +115,19 @@ export function filterByManagedPolicy(plugins, managed) {
|
|
|
115
115
|
continue;
|
|
116
116
|
}
|
|
117
117
|
if (requireSigned) {
|
|
118
|
+
// Fail closed when no signing keys are pinned: a lock's signature can be
|
|
119
|
+
// SELF-signed by whoever authored the plugin (the lock lives in a
|
|
120
|
+
// writable dir), so "signed by anyone" is not a guarantee. Without
|
|
121
|
+
// trustedPluginKeySha256 the gate would accept any self-signed drop-in.
|
|
122
|
+
if (!trustedKeys || trustedKeys.size === 0) {
|
|
123
|
+
dropped.push({
|
|
124
|
+
name: p.name,
|
|
125
|
+
reason:
|
|
126
|
+
"requireSignedPlugins: no trustedPluginKeySha256 fingerprints " +
|
|
127
|
+
"configured (fail-closed — pin the org's signing keys)",
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
118
131
|
const v = verifyInstalledSignature(p, { trustedKeys });
|
|
119
132
|
if (!v.signed) {
|
|
120
133
|
dropped.push({
|
|
@@ -46,6 +46,39 @@ export function isRemoteSource(raw) {
|
|
|
46
46
|
return /^https?:\/\//i.test(s) && /\.json(\?.*)?(#.*)?$/i.test(s);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Enforce HTTPS for registry URLs. The registry supplies BOTH the git source
|
|
51
|
+
* and its claimed sha256 — over plain HTTP a network MITM controls the two
|
|
52
|
+
* together, so the integrity check verifies nothing. Loopback is exempt (a
|
|
53
|
+
* local dev registry isn't MITM-able off-host); anything else needs the
|
|
54
|
+
* explicit opt-in (opts.allowInsecure / CC_PLUGIN_REGISTRY_ALLOW_HTTP=1).
|
|
55
|
+
*/
|
|
56
|
+
export function assertRegistryUrlSafe(url, { allowInsecure = false } = {}) {
|
|
57
|
+
let u;
|
|
58
|
+
try {
|
|
59
|
+
u = new URL(String(url));
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error(`invalid registry URL: ${url}`);
|
|
62
|
+
}
|
|
63
|
+
if (u.protocol === "https:") return;
|
|
64
|
+
if (u.protocol !== "http:") {
|
|
65
|
+
throw new Error(`registry URL must be http(s): ${url}`);
|
|
66
|
+
}
|
|
67
|
+
// WHATWG URL keeps the brackets on IPv6 hostnames — strip before comparing.
|
|
68
|
+
const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
69
|
+
const loopback =
|
|
70
|
+
host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
71
|
+
const optIn =
|
|
72
|
+
allowInsecure === true || process.env.CC_PLUGIN_REGISTRY_ALLOW_HTTP === "1";
|
|
73
|
+
if (loopback || optIn) return;
|
|
74
|
+
throw new Error(
|
|
75
|
+
`plain-HTTP registry rejected: ${url} — a network MITM controls both the ` +
|
|
76
|
+
`source and its sha256, so the integrity check verifies nothing. Use ` +
|
|
77
|
+
`https, or opt in with --allow-insecure-registry / ` +
|
|
78
|
+
`CC_PLUGIN_REGISTRY_ALLOW_HTTP=1 on a trusted network.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
49
82
|
/** Where a fetched registry is cached (content-addressed by its URL). */
|
|
50
83
|
export function registryCachePath(url, cacheDir) {
|
|
51
84
|
const dir =
|
|
@@ -93,6 +126,7 @@ export async function fetchRegistry(url, opts = {}) {
|
|
|
93
126
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
94
127
|
allowCache = true,
|
|
95
128
|
} = opts;
|
|
129
|
+
assertRegistryUrlSafe(url, { allowInsecure: opts.allowInsecure });
|
|
96
130
|
const cachePath = registryCachePath(url, cacheDir);
|
|
97
131
|
const headers = { Accept: "application/json" };
|
|
98
132
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
@@ -7,20 +7,24 @@
|
|
|
7
7
|
* install records the verification result in a `.plugin-lock.json` inside the
|
|
8
8
|
* immutable version dir; discovery then re-checks it fail-closed.
|
|
9
9
|
*
|
|
10
|
-
* The lock
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The lock persists the DETACHED SIGNATURE + PUBLIC KEY themselves (lockVersion
|
|
11
|
+
* 2), and load-time verification re-runs the actual Ed25519 verify over the
|
|
12
|
+
* on-disk manifest bytes. The lock file lives in a writable plugin dir, so
|
|
13
|
+
* nothing in it can be TRUSTED — a recorded boolean or fingerprint could simply
|
|
14
|
+
* be forged by whoever authored the plugin. What a forger CANNOT do is produce a
|
|
15
|
+
* signature that verifies under a key the org has pinned via
|
|
16
|
+
* trustedPluginKeySha256 — which is why the key fingerprint used for the trust
|
|
17
|
+
* check is COMPUTED from the embedded public key, never read from the lock.
|
|
18
|
+
* The signature covers only the manifest file, not every component file — a
|
|
14
19
|
* known limitation (documented), an integrity anchor rather than a full
|
|
15
|
-
* supply-chain seal; the
|
|
16
|
-
* tampering, which is where the component wiring is declared.
|
|
20
|
+
* supply-chain seal; the manifest is where the component wiring is declared.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
23
|
+
import { createHash, createPublicKey, verify } from "node:crypto";
|
|
20
24
|
import fs from "fs";
|
|
21
25
|
import path from "path";
|
|
22
26
|
|
|
23
|
-
const
|
|
27
|
+
export const LOCK_FILENAME = ".plugin-lock.json";
|
|
24
28
|
|
|
25
29
|
export const _deps = {
|
|
26
30
|
readFileSync: fs.readFileSync,
|
|
@@ -31,18 +35,29 @@ export const _deps = {
|
|
|
31
35
|
/** Record a verified signature into the version dir. */
|
|
32
36
|
export function writePluginLock(
|
|
33
37
|
versionDir,
|
|
34
|
-
{
|
|
38
|
+
{
|
|
39
|
+
manifestFile,
|
|
40
|
+
sha256,
|
|
41
|
+
publicKeySha256,
|
|
42
|
+
signatureVerified,
|
|
43
|
+
signatureBase64,
|
|
44
|
+
publicKeyPem,
|
|
45
|
+
},
|
|
35
46
|
) {
|
|
36
47
|
const rel = path.relative(versionDir, manifestFile).replace(/\\/g, "/");
|
|
37
48
|
const lock = {
|
|
38
|
-
lockVersion:
|
|
49
|
+
lockVersion: 2,
|
|
39
50
|
manifest: rel || "plugin.json",
|
|
40
51
|
sha256,
|
|
41
52
|
publicKeySha256: publicKeySha256 || null,
|
|
42
53
|
signatureVerified: signatureVerified === true,
|
|
54
|
+
// The material load-time enforcement actually re-verifies. Without these a
|
|
55
|
+
// lock is just self-asserted JSON and counts as unsigned.
|
|
56
|
+
signatureBase64: signatureBase64 || null,
|
|
57
|
+
publicKeyPem: publicKeyPem || null,
|
|
43
58
|
};
|
|
44
59
|
_deps.writeFileSync(
|
|
45
|
-
path.join(versionDir,
|
|
60
|
+
path.join(versionDir, LOCK_FILENAME),
|
|
46
61
|
JSON.stringify(lock, null, 2),
|
|
47
62
|
"utf8",
|
|
48
63
|
);
|
|
@@ -51,7 +66,7 @@ export function writePluginLock(
|
|
|
51
66
|
|
|
52
67
|
/** Read a version dir's signature lock, or null when absent/unreadable. */
|
|
53
68
|
export function readPluginLock(versionDir) {
|
|
54
|
-
const p = path.join(versionDir,
|
|
69
|
+
const p = path.join(versionDir, LOCK_FILENAME);
|
|
55
70
|
if (!_deps.existsSync(p)) return null;
|
|
56
71
|
try {
|
|
57
72
|
return JSON.parse(_deps.readFileSync(p, "utf8"));
|
|
@@ -62,9 +77,11 @@ export function readPluginLock(versionDir) {
|
|
|
62
77
|
|
|
63
78
|
/**
|
|
64
79
|
* Verify an installed plugin's recorded signature against its on-disk manifest.
|
|
65
|
-
* `signed:true` ONLY when a lock exists,
|
|
66
|
-
*
|
|
67
|
-
* is a non-empty Set — the signing key fingerprint
|
|
80
|
+
* `signed:true` ONLY when a lock exists, carries real signature material, the
|
|
81
|
+
* embedded Ed25519 signature VERIFIES over the on-disk manifest bytes, and —
|
|
82
|
+
* when trustedKeys is a non-empty Set — the signing key's COMPUTED fingerprint
|
|
83
|
+
* is among them. Every field of the lock is treated as attacker-writable; the
|
|
84
|
+
* only trust anchors are the cryptographic verify and the caller's trustedKeys.
|
|
68
85
|
*
|
|
69
86
|
* @param {{root:string}} plugin
|
|
70
87
|
* @param {object} [opts] { trustedKeys?: Set<string> }
|
|
@@ -96,28 +113,68 @@ export function verifyInstalledSignature(plugin, opts = {}) {
|
|
|
96
113
|
if (!_deps.existsSync(manifestFile)) {
|
|
97
114
|
return { signed: false, reason: "locked manifest file is missing" };
|
|
98
115
|
}
|
|
99
|
-
let
|
|
116
|
+
let bytes;
|
|
100
117
|
try {
|
|
101
|
-
|
|
102
|
-
.update(_deps.readFileSync(manifestFile))
|
|
103
|
-
.digest("hex");
|
|
118
|
+
bytes = _deps.readFileSync(manifestFile);
|
|
104
119
|
} catch {
|
|
105
120
|
return { signed: false, reason: "manifest unreadable" };
|
|
106
121
|
}
|
|
122
|
+
const sha = createHash("sha256").update(bytes).digest("hex");
|
|
107
123
|
if (sha.toLowerCase() !== String(lock.sha256 || "").toLowerCase()) {
|
|
108
124
|
return {
|
|
109
125
|
signed: false,
|
|
110
126
|
reason: "manifest tampered since install (sha256 mismatch)",
|
|
111
127
|
};
|
|
112
128
|
}
|
|
129
|
+
// A lock without real signature material is unverifiable, and therefore
|
|
130
|
+
// worthless as a signing gate: `signatureVerified:true` is just a JSON field
|
|
131
|
+
// anyone can write. Legacy (lockVersion 1) locks land here too — reinstalling
|
|
132
|
+
// the plugin with --signature re-records a verifiable lock.
|
|
133
|
+
if (!lock.signatureBase64 || !lock.publicKeyPem) {
|
|
134
|
+
return {
|
|
135
|
+
signed: false,
|
|
136
|
+
reason:
|
|
137
|
+
"lock lacks signature material (legacy or hand-written lock — " +
|
|
138
|
+
"reinstall the plugin with --signature to re-record it)",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
let keyObject;
|
|
142
|
+
let publicKeySha256;
|
|
143
|
+
try {
|
|
144
|
+
keyObject = createPublicKey(lock.publicKeyPem);
|
|
145
|
+
// Fingerprint is COMPUTED from the embedded key — the lock's own
|
|
146
|
+
// publicKeySha256 field is attacker-writable and never used for trust.
|
|
147
|
+
publicKeySha256 = createHash("sha256")
|
|
148
|
+
.update(keyObject.export({ type: "spki", format: "der" }))
|
|
149
|
+
.digest("hex");
|
|
150
|
+
} catch {
|
|
151
|
+
return { signed: false, reason: "lock public key is not a valid key" };
|
|
152
|
+
}
|
|
153
|
+
let sigOk = false;
|
|
154
|
+
try {
|
|
155
|
+
sigOk = verify(
|
|
156
|
+
null,
|
|
157
|
+
bytes,
|
|
158
|
+
keyObject,
|
|
159
|
+
Buffer.from(String(lock.signatureBase64), "base64"),
|
|
160
|
+
);
|
|
161
|
+
} catch {
|
|
162
|
+
sigOk = false;
|
|
163
|
+
}
|
|
164
|
+
if (!sigOk) {
|
|
165
|
+
return {
|
|
166
|
+
signed: false,
|
|
167
|
+
reason: "recorded signature does not verify over the on-disk manifest",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
113
170
|
const trustedKeys = opts.trustedKeys;
|
|
114
171
|
if (trustedKeys && trustedKeys.size > 0) {
|
|
115
|
-
if (!
|
|
172
|
+
if (!trustedKeys.has(publicKeySha256)) {
|
|
116
173
|
return {
|
|
117
174
|
signed: false,
|
|
118
|
-
reason: `signing key not trusted (${
|
|
175
|
+
reason: `signing key not trusted (${publicKeySha256})`,
|
|
119
176
|
};
|
|
120
177
|
}
|
|
121
178
|
}
|
|
122
|
-
return { signed: true, publicKeySha256
|
|
179
|
+
return { signed: true, publicKeySha256 };
|
|
123
180
|
}
|