diff --git a/.github/linters/package-lock.json b/.github/linters/package-lock.json index 158651b9..eaeada66 100644 --- a/.github/linters/package-lock.json +++ b/.github/linters/package-lock.json @@ -15,7 +15,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -28,7 +27,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", "engines": { "node": ">= 8" } @@ -37,7 +35,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -50,7 +47,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "license": "MIT", "engines": { "node": ">=18" }, @@ -62,7 +58,6 @@ "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", "dependencies": { "@types/ms": "*" } @@ -70,20 +65,17 @@ "node_modules/@types/katex": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", - "license": "MIT" + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==" }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, "node_modules/@types/unist": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" }, "node_modules/ansi-regex": { "version": "6.4.0", @@ -107,7 +99,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -119,7 +110,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -129,7 +119,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -139,7 +128,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -149,7 +137,6 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", "engines": { "node": ">= 12" } @@ -158,7 +145,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -175,7 +161,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", "dependencies": { "character-entities": "^2.0.0" }, @@ -188,7 +173,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -197,7 +181,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", "dependencies": { "dequal": "^2.0.0" }, @@ -206,23 +189,10 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -247,7 +217,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -271,7 +240,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -301,9 +269,9 @@ } }, "node_modules/ignore": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", - "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.11.tgz", + "integrity": "sha512-YChdK5txDjwGUvgR7oCJcLGkwi3LDh8Zx8tS7ndYciLEg/oOCL26VUBSUEtgS7EjiybcBqFxh5j0rAdDX7/bbg==", "license": "MIT", "engines": { "node": ">= 4" @@ -313,7 +281,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -323,7 +290,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" @@ -337,7 +303,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -347,7 +312,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -356,7 +320,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -368,7 +331,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -378,7 +340,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -387,7 +348,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", - "license": "MIT", "engines": { "node": ">=12" }, @@ -440,7 +400,6 @@ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], - "license": "MIT", "dependencies": { "commander": "^8.3.0" }, @@ -467,33 +426,6 @@ "uc.micro": "^2.0.0" } }, - "node_modules/markdown-it": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz", - "integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, "node_modules/markdownlint": { "version": "0.41.1", "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.41.1.tgz", @@ -555,17 +487,52 @@ "markdownlint-cli2": ">=0.0.4" } }, + "node_modules/markdownlint-cli2/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdownlint-cli2/node_modules/markdown-it": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz", + "integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", "engines": { "node": ">= 8" } @@ -584,7 +551,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -619,7 +585,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", @@ -643,7 +608,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", @@ -662,7 +626,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", @@ -678,7 +641,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", @@ -698,7 +660,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", @@ -715,7 +676,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", - "license": "MIT", "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", @@ -744,7 +704,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -765,7 +724,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", @@ -787,7 +745,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -807,7 +764,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -829,7 +785,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", @@ -851,7 +806,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -871,7 +825,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } @@ -890,7 +843,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", @@ -911,7 +863,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" @@ -931,7 +882,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } @@ -949,8 +899,7 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-html-tag-name": { "version": "2.0.1", @@ -965,8 +914,7 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-normalize-identifier": { "version": "2.0.1", @@ -982,7 +930,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-symbol": "^2.0.0" } @@ -1001,7 +948,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-types": "^2.0.0" } @@ -1020,7 +966,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", @@ -1041,7 +986,6 @@ "url": "https://opencollective.com/unified" } ], - "license": "MIT", "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", @@ -1062,8 +1006,7 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromark-util-types": { "version": "2.0.2", @@ -1078,14 +1021,12 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ], - "license": "MIT" + ] }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -1097,14 +1038,12 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/parse-entities": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", @@ -1123,7 +1062,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -1157,14 +1095,12 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "license": "MIT" + ] }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -1188,7 +1124,6 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -1197,7 +1132,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "license": "MIT", "engines": { "node": ">=14.16" }, @@ -1237,7 +1171,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" }, @@ -1252,7 +1185,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, diff --git a/integrations/isolation/acq-kits/paseo/README.md b/integrations/isolation/acq-kits/paseo/README.md index 8c9a2cb7..f63ffeb0 100644 --- a/integrations/isolation/acq-kits/paseo/README.md +++ b/integrations/isolation/acq-kits/paseo/README.md @@ -131,8 +131,7 @@ This kit does the best achievable thing: it pins the one global root to a subdirectory of the **first mounted project directory**. When you run the `opencode` wrapper on the `acq run` path (whose working directory is that primary workspace), the kit shim writes `worktrees.root = /.paseo-worktrees` -into `config.json` and **restarts the daemon** so the change takes effect (Paseo -reads `worktrees.root` only at daemon startup — there is no signal to reread it). +into `config.json` and **restarts the daemon worker** so the change takes effect. Before you run it once, worktrees default to `$PASEO_HOME/worktrees`. To override the location, pre-set `worktrees.root` to an absolute path in diff --git a/integrations/isolation/acq-kits/paseo/TROUBLESHOOTING.md b/integrations/isolation/acq-kits/paseo/TROUBLESHOOTING.md index d5a62788..c06083b4 100644 --- a/integrations/isolation/acq-kits/paseo/TROUBLESHOOTING.md +++ b/integrations/isolation/acq-kits/paseo/TROUBLESHOOTING.md @@ -306,15 +306,11 @@ acq exec -- sh -c 'cat "${PASEO_HOME:-$HOME/.paseo}/config.json"' `acq run opencode ` once. - `worktrees.root` points at `/.paseo-worktrees` but new worktrees still land elsewhere → the daemon may not have restarted to pick it up. The wrapper - bounces the daemon only when the value **changes**; force a bounce by killing - the daemon child (the supervisor respawns it): + restarts the daemon only when the value **changes**; force a restart with the + supported CLI command: ```bash - acq exec -- sh -c ' - for pid in $(pgrep -u "$(id -u)" -f "paseo daemon start"); do - tr "\0" " " -- paseo daemon restart --home /home/agent/.paseo ``` - **The root must be absolute.** A relative `worktrees.root` resolves against @@ -353,18 +349,15 @@ serves the API + WebSocket + UI locally; `curl http://127.0.0.1:6767/api/health` returns 200 throughout. If the **host** browser sees a refused connection, that is a port-mapping issue (see "The browser UI never loads" above), not the relay. -**Fix.** The kit now sets `PASEO_RELAY_ENABLED=false` in `spec.yaml` (read at -daemon config load, precedence over persisted config, survives restarts/self- -updates), so the relay is off by default. If you are running an older sandbox that -predates this, disable it live and bounce the daemon: +**Fix.** The kit sets `PASEO_RELAY_ENABLED=false` in `spec.yaml` and persists +`daemon.relay.enabled=false` in config.json for Paseo 0.9 managed/config tooling, +so the relay is off by default. If you are running an older sandbox that predates +this, disable it live and restart the daemon: ```bash acq exec -- sh -c ' - node -e "const f=process.env.HOME+\"/.paseo/config.json\";const c=require(f);(c.daemon??={}).relay={enabled:false};require(\"fs\").writeFileSync(f,JSON.stringify(c,null,2)+\"\n\",{mode:0o600})" - for pid in $(pgrep -u "$(id -u)" -f "paseo daemon start"); do - tr "\0" " " /.paseo-worktrees`, and writes - it to `config.json`. **If the value changed**, it restarts the daemon so the new - root takes effect. (The pin/bounce lives in the shim; see + it to `config.json`. **If the value changed**, it restarts the daemon worker so + the new root takes effect. (The pin/restart lives in the shim; see `paseo-mixin-generic-wrapper.md` for the wrapper/shim split.) ### The bounce mechanism (why it's safe) @@ -44,21 +44,23 @@ The process tree is: ``` sh -c (marker: supervisor:paseo-daemon) ← the startup script's respawn loop - └─ paseo daemon start --foreground ← node CLI + └─ paseo daemon run ← node CLI └─ node supervisor-entrypoint.js ← "Paseo Supervisor"; holds $PASEO_HOME/paseo.pid └─ node daemon-worker.js ← the listener on :6767 (SIGTERM = graceful) ``` -The wrapper kills the PIDs whose argv matches `paseo daemon start` **but do NOT -carry the `supervisor:paseo-daemon` marker** — i.e. the Paseo-owned tree, never -our shell respawn loop. `kill` (SIGTERM) lets `daemon-worker` shut down -gracefully, free `:6767`, and release the PID lock. Our shell loop then respawns -`paseo daemon start`, which re-reads `config.json` and picks up the new -`worktrees.root`. +The wrapper first uses `paseo daemon restart`, which asks Paseo's own supervisor +to restart the worker and re-read `config.json`. If that command cannot run, the +fallback kills PIDs whose argv matches `paseo daemon run` **but do NOT carry the +`supervisor:paseo-daemon` marker** — i.e. the Paseo-owned tree, never our shell +respawn loop. `kill` (SIGTERM) lets `daemon-worker` shut down gracefully and free +`:6767`. The fallback clears `paseo.pid` only after it actually kills a daemon +process, so it does not delete a live worker's lock during ordinary respawn. -The PID lock is stale-tolerant (`acquirePidLock` reclaims a dead owner's lock and -treats a >5-min-old lock as stale), so even an ungraceful death does not deadlock -the relaunch. +The normal `paseo daemon restart` path keeps the outer supervisor and PID lock +intact; the marker-scoped kill is only a recovery fallback. Both startup config +writes and wrapper worktree-root writes use the same coarse config lock before +modifying `$PASEO_HOME/config.json`. ## Alternatives considered @@ -79,8 +81,8 @@ the relaunch. daemon bounce (idempotent; no bounce if unchanged). - Two independent "supervisors" exist conceptually — our shell respawn loop and Paseo's own `supervisor-entrypoint.js` — but they nest cleanly (ours respawns - the whole `paseo daemon start` tree; Paseo's manages the worker within one - start). The kill is marker-scoped so our loop is never the target. + the whole `paseo daemon run` tree; Paseo's manages the worker within one run). + The fallback kill is marker-scoped so our loop is never the target. ## Links diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim b/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim index d34dcbb7..b6989a1a 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-agent-shim @@ -23,11 +23,10 @@ # relative root resolves against PASEO_HOME. On `acq run ` the entrypoint # runs with cwd = the primary (first mounted) workspace, so the shim computes # /.paseo-worktrees and writes it to $PASEO_HOME/config.json (via -# paseo-set-worktrees-root.mjs). The daemon reads worktrees.root ONLY at boot -# (no SIGHUP / no runtime reread), so when the value CHANGES the shim bounces -# the daemon — it kills the supervised daemon child (never the supervisor), -# and the startup script's supervisor:paseo-daemon loop respawns it, re-reading -# the new root. +# paseo-set-worktrees-root.mjs). The daemon reads worktrees.root at worker +# start, so when the value CHANGES the shim restarts the daemon worker via +# `paseo daemon restart` (falling back to a marker-scoped process kill only +# if the CLI restart cannot run). # 2. PRINT HOST-CONNECT INSTRUCTIONS and, when it IS PID 1 (the `acq run` # entrypoint), HOLD the foreground so that session's sandbox — and thus the # daemon/UI — stays up. @@ -79,13 +78,12 @@ if [ -f "$SET_ROOT_SCRIPT" ] && command -v node >/dev/null 2>&1; then esac fi -# 2. If the root changed, bounce the daemon so it re-reads config.json. We kill -# ONLY the daemon process tree, never the startup script's supervisor loop, so -# the supervisor relaunches the daemon with the new worktrees.root. The -# supervisor loop's argv carries the marker "supervisor:paseo-daemon"; the -# actual daemon is a `paseo daemon start --foreground ...` CHILD of that loop. -# We select PIDs whose argv matches the daemon-start pattern but EXCLUDE the -# supervisor marker, and kill just those. +# 2. If the root changed, restart the daemon worker so it re-reads config.json. +# Paseo 0.9 exposes `paseo daemon restart` for this. If the daemon is wedged or +# the CLI surface changes again, fall back to killing ONLY the supervised Paseo +# tree, never the startup script's supervisor loop. The supervisor loop's argv +# carries the marker "supervisor:paseo-daemon"; the actual daemon child is a +# `paseo daemon run` tree beneath that loop. daemon_up() { # /api/health is unauthenticated and returns 200 when the daemon is listening. curl -fsS "http://127.0.0.1:$PASEO_PORT/api/health" >/dev/null 2>&1 @@ -130,14 +128,23 @@ ui_ready() { } if [ -n "$worktrees_changed" ]; then echo "paseo: pinned Paseo worktree root to $WORKTREES_ROOT; restarting the daemon to apply it ..." - for _pid in $(pgrep -u "$(id -u)" -f "paseo daemon start" 2>/dev/null); do - # Skip the supervisor loop (its cmdline carries the marker). - if tr '\0' ' ' < "/proc/$_pid/cmdline" 2>/dev/null | grep -q "supervisor:paseo-daemon"; then - continue - fi - kill "$_pid" 2>/dev/null || true - done - # Wait for the supervisor to bring it back on the new root (bounded). + if command -v paseo >/dev/null 2>&1 && paseo daemon restart --home "$PASEO_HOME_DIR" --timeout 30 >/dev/null 2>&1; then + : + else + echo "paseo: daemon restart command failed; falling back to supervisor respawn" >&2 + killed_daemon="" + for _pid in $(pgrep -u "$(id -u)" -f "paseo daemon run" 2>/dev/null); do + # Skip the supervisor loop (its cmdline carries the marker). + if tr '\0' ' ' < "/proc/$_pid/cmdline" 2>/dev/null | grep -q "supervisor:paseo-daemon"; then + continue + fi + if kill "$_pid" 2>/dev/null; then + killed_daemon=yes + fi + done + [ -n "$killed_daemon" ] && rm -f "$PASEO_HOME_DIR/paseo.pid" 2>/dev/null || true + fi + # Wait for the daemon to return on the new root (bounded). _i=0 while [ "$_i" -lt 30 ]; do daemon_up && break diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-assert-daemon-config.mjs b/integrations/isolation/acq-kits/paseo/files/home/paseo-assert-daemon-config.mjs new file mode 100644 index 00000000..cc23f05c --- /dev/null +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-assert-daemon-config.mjs @@ -0,0 +1,68 @@ +#!/usr/bin/env node +// paseo-assert-daemon-config.mjs — fail closed unless config.json contains the +// daemon settings this kit requires before launching an unauthenticated daemon. + +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +function resolvePaseoHome(explicit) { + if (explicit && explicit.trim()) return path.resolve(explicit.trim()); + const env = process.env.PASEO_HOME; + if (env && env.trim()) { + const raw = env.trim(); + if (raw === "~") return homedir(); + if (raw.startsWith("~/")) return path.join(homedir(), raw.slice(2)); + return path.resolve(raw); + } + return path.join(homedir(), ".paseo"); +} + +function parseArgs(argv) { + const args = { paseoHome: undefined, listen: undefined }; + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === "--paseo-home") { + args.paseoHome = argv[++i]; + } else if (arg === "--listen") { + args.listen = argv[++i]; + } else { + throw new Error(`unknown argument: ${arg}`); + } + } + if (!args.listen) throw new Error("missing required --listen "); + return args; +} + +export function assertDaemonConfig({ paseoHome, listen }) { + const configPath = path.join(resolvePaseoHome(paseoHome), "config.json"); + let config; + try { + config = JSON.parse(readFileSync(configPath, "utf8")); + } catch (err) { + throw new Error(`cannot read required daemon config ${configPath}: ${err.message}`); + } + + const failures = []; + if (config?.daemon?.listen !== listen) { + failures.push(`daemon.listen=${JSON.stringify(config?.daemon?.listen)}`); + } + if (config?.features?.webUi?.enabled !== true) { + failures.push(`features.webUi.enabled=${JSON.stringify(config?.features?.webUi?.enabled)}`); + } + if (config?.daemon?.relay?.enabled !== false) { + failures.push(`daemon.relay.enabled=${JSON.stringify(config?.daemon?.relay?.enabled)}`); + } + if (failures.length > 0) { + throw new Error(`config.json missing required daemon settings: ${failures.join(", ")}`); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + try { + assertDaemonConfig(parseArgs(process.argv.slice(2))); + } catch (err) { + process.stderr.write(`error: ${err.message}\n`); + process.exit(1); + } +} diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-register-mounts.mjs b/integrations/isolation/acq-kits/paseo/files/home/paseo-register-mounts.mjs index 2f6ea579..4f30f470 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-register-mounts.mjs +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-register-mounts.mjs @@ -225,7 +225,12 @@ async function main() { let client; try { - client = await connectToDaemon({}); + client = await connectToDaemon({ + target: { + kind: "instance", + home: process.env.PASEO_HOME || `${process.env.HOME}/.paseo`, + }, + }); } catch (err) { log(`could not connect to the Paseo daemon: ${err.message}`); return; // fail-open (daemon may still be coming up on a very early boot) diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-set-worktrees-root.mjs b/integrations/isolation/acq-kits/paseo/files/home/paseo-set-worktrees-root.mjs index 10f0a566..2dd99a80 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-set-worktrees-root.mjs +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-set-worktrees-root.mjs @@ -24,10 +24,11 @@ // SAFETY: this is a read-modify-write of a JSON file. It preserves every existing // key, only sets/overwrites `worktrees.root` (and creates the `worktrees` object // if absent). It refuses a non-absolute root (Paseo would resolve it against -// PASEO_HOME, defeating the purpose). It writes atomically (temp file + rename) -// with private 0600 perms, matching how Paseo itself stores config.json. +// PASEO_HOME, defeating the purpose). It takes the same coarse lock used by the +// startup script, then writes atomically (temp file + rename) with private 0600 +// perms, matching how Paseo itself stores config.json. -import { readFileSync, writeFileSync, mkdirSync, renameSync, existsSync } from "node:fs"; +import { mkdirSync, readFileSync, renameSync, rmdirSync, writeFileSync, existsSync } from "node:fs"; import { homedir } from "node:os"; import path from "node:path"; @@ -59,16 +60,33 @@ function resolvePaseoHome(explicit) { return path.join(homedir(), ".paseo"); } -function main() { - const { root, paseoHome: paseoHomeArg } = parseArgs(process.argv.slice(2)); - - if (!path.isAbsolute(root)) { - // A relative root would be resolved by Paseo against PASEO_HOME, not the - // project — refuse rather than silently mis-place worktrees. - throw new Error(`--root must be an absolute path (got: ${root})`); +function withConfigLock(paseoHome, fn) { + const lockDir = path.join(paseoHome, ".config-json.lock"); + const timeoutMs = Number.parseInt(process.env.PASEO_CONFIG_LOCK_TIMEOUT_MS || "30000", 10); + const deadline = Date.now() + timeoutMs; + while (true) { + try { + mkdirSync(lockDir); + break; + } catch (err) { + if (err.code !== "EEXIST" || Date.now() >= deadline) { + throw new Error(`cannot lock ${path.join(paseoHome, "config.json")}: ${err.message}`); + } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250); + } + } + try { + return fn(); + } finally { + try { + rmdirSync(lockDir); + } catch { + // Best effort cleanup; a later run times out rather than racing config writes. + } } +} - const paseoHome = resolvePaseoHome(paseoHomeArg); +function updateConfig(root, paseoHome) { const configPath = path.join(paseoHome, "config.json"); // Read the existing config if present. The daemon initializes config.json on @@ -128,6 +146,20 @@ function main() { process.stdout.write("changed\n"); } +function main() { + const { root, paseoHome: paseoHomeArg } = parseArgs(process.argv.slice(2)); + + if (!path.isAbsolute(root)) { + // A relative root would be resolved by Paseo against PASEO_HOME, not the + // project — refuse rather than silently mis-place worktrees. + throw new Error(`--root must be an absolute path (got: ${root})`); + } + + const paseoHome = resolvePaseoHome(paseoHomeArg); + mkdirSync(paseoHome, { recursive: true }); + withConfigLock(paseoHome, () => updateConfig(root, paseoHome)); +} + try { main(); } catch (err) { diff --git a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh index cb609245..1d5af04c 100755 --- a/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh +++ b/integrations/isolation/acq-kits/paseo/files/home/paseo-start.sh @@ -3,10 +3,11 @@ # daemon (which serves the API, the WebSocket, AND the bundled web UI on one port) # so it auto-restarts if it exits. # -# SCOPE: this startup script manages the Paseo daemon only. It supervises a single -# `paseo daemon start --foreground --listen 0.0.0.0:6767 --web-ui`. The daemon -# runs under a respawn loop, so a crash, a self-update, or a wrapper-triggered -# bounce (to apply a new worktrees.root) self-heals. Because this script runs as a +# SCOPE: this startup script manages the Paseo daemon only. It persists the +# daemon settings Paseo 0.9 reads from config.json, then supervises a single +# `paseo daemon run`. The daemon runs under a respawn loop, so a crash, a +# self-update, or a wrapper-triggered restart (to apply a new worktrees.root) +# self-heals. Because this script runs as a # `startup` command — which fires on EVERY sandbox start, including a detached # `acq create` with nobody attached, and runs under the sandbox's tini keepalive # (PID 1), independent of any interactive session — the daemon + UI come up on @@ -31,7 +32,7 @@ # # Pins are provided via the environment, with an in-script fallback default kept # in sync with the kit spec's documented pin: -# PASEO_CLI_VERSION — @getpaseo/cli version to install (default 0.7.0) +# PASEO_CLI_VERSION — @getpaseo/cli version to install (default 0.10.2) # PASEO_LISTEN — daemon bind address (default 0.0.0.0:6767) # PASEO_RESTART_DELAY — seconds to wait before respawning the daemon (default 5) @@ -53,6 +54,8 @@ PASEO_PORT="$(printf '%s' "$PASEO_LISTEN" | sed -n 's/.*:\([0-9][0-9]*\)$/\1/p') # directory with the daemon so the web UI pre-lists them). Backend-agnostic; see # the file header and docs/decisions/prepopulate-projects-from-mounts.md. REGISTER_MOUNTS_SCRIPT="${HOME}/paseo-register-mounts.mjs" +STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/paseo" +mkdir -p "$STATE_DIR" 2>/dev/null || true # npm global installs land in this prefix's bin, which is NOT on the startup # shell's PATH by default. Put both possible bins on PATH up front so a @@ -75,7 +78,7 @@ case ":$PATH:" in *":$HOME/.npm-global/bin:"*) : ;; *) PATH="$HOME/.npm-global/b export PATH # --- Install the pinned Paseo CLI if it is missing or out of date. ----------- -_ver="${PASEO_CLI_VERSION:-0.7.0}" +_ver="${PASEO_CLI_VERSION:-0.10.2}" _installed_ver="" if command -v paseo >/dev/null 2>&1; then _installed_ver="$(paseo --version 2>/dev/null | sed -n 's/^v//; s/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p; q')" @@ -142,15 +145,66 @@ command -v paseo >/dev/null 2>&1 || { exit 0 # never fail the sandbox over an optional UI } +# --- Persist daemon configuration that Paseo 0.9 no longer accepts as flags. --- +# Paseo 0.9 removed `paseo daemon start --listen/--web-ui/--foreground` and, in +# managed `daemon start` mode, strips PASEO_* daemon-setting env vars before the +# worker starts. Persist the settings the sandbox depends on so both `daemon run` +# and any local status/config tooling agree on the expected shape. +PASEO_HOME_DIR="${PASEO_HOME:-$HOME/.paseo}" +mkdir -p "$PASEO_HOME_DIR" 2>/dev/null || true +CONFIG_LOCK_DIR="$PASEO_HOME_DIR/.config-json.lock" +_have_config_lock="" +cleanup_config_lock() { + [ "$_have_config_lock" = yes ] && rmdir "$CONFIG_LOCK_DIR" 2>/dev/null || true +} +trap cleanup_config_lock EXIT HUP INT TERM +acquire_config_lock() { + _deadline=$(( $(date +%s) + 30 )) + while ! mkdir "$CONFIG_LOCK_DIR" 2>/dev/null; do + [ "$(date +%s)" -ge "$_deadline" ] && return 1 + sleep 1 + done + _have_config_lock=yes +} +release_config_lock() { + cleanup_config_lock + _have_config_lock="" +} + +config_ok=yes +if ! acquire_config_lock; then + echo "paseo: error: could not lock $PASEO_HOME_DIR/config.json; not starting unverified daemon" >&2 + exit 0 +fi +if ! paseo daemon config set --home "$PASEO_HOME_DIR" --string daemon.listen "$PASEO_LISTEN" >>"$STATE_DIR/paseo-config.log" 2>&1; then + echo "paseo: error: could not persist daemon.listen=$PASEO_LISTEN; see $STATE_DIR/paseo-config.log" >&2 + config_ok="" +fi +if ! paseo daemon config set --home "$PASEO_HOME_DIR" features.webUi.enabled true >>"$STATE_DIR/paseo-config.log" 2>&1; then + echo "paseo: error: could not persist features.webUi.enabled=true; see $STATE_DIR/paseo-config.log" >&2 + config_ok="" +fi +if ! paseo daemon config set --home "$PASEO_HOME_DIR" daemon.relay.enabled false >>"$STATE_DIR/paseo-config.log" 2>&1; then + echo "paseo: error: could not persist daemon.relay.enabled=false; see $STATE_DIR/paseo-config.log" >&2 + config_ok="" +fi +if ! node "$HOME/paseo-assert-daemon-config.mjs" --paseo-home "$PASEO_HOME_DIR" --listen "$PASEO_LISTEN" >>"$STATE_DIR/paseo-config.log" 2>&1; then + echo "paseo: error: config.json does not contain required daemon settings; not starting unverified daemon" >&2 + config_ok="" +fi +release_config_lock +if [ -z "$config_ok" ]; then + exit 0 +fi + # --- Supervise the Paseo daemon (idempotent). -------------------------------- # The daemon runs UNSECURED (no PASEO_PASSWORD) — the sandbox is the security -# boundary, and the published host port is loopback only. `--foreground` keeps it -# in the foreground so the supervisor can see it exit and restart it (after a -# crash, a self-update, or a wrapper-triggered bounce to apply a new -# worktrees.root). `--web-ui` (redundant with PASEO_WEB_UI_ENABLED=true from the -# kit env, set for belt-and-suspenders) serves the bundled browser UI on the same -# port. No systemd — this script is launched in the background by the kit's -# startup command, so the supervisor loop runs backgrounded here and lives for the +# boundary, and the published host port is loopback only. `daemon run` is Paseo +# 0.9's foreground/deployment launch path: it keeps daemon-setting env overrides +# available and runs Paseo's own supervisor-entrypoint (IPC restart + crash +# restart) in the foreground, so our outer loop can still see it exit and relaunch +# it. No systemd — this script is launched in the background by the kit's startup +# command, so the supervisor loop runs backgrounded here and lives for the # container's lifetime, held open by the sandbox's tini keepalive (PID 1) # regardless of whether anyone attaches. # @@ -168,14 +222,17 @@ supervisor_running() { pgrep -u "$(id -u)" -f "supervisor:$1" >/dev/null 2>&1; } DAEMON_LOG="$HOME/.local/state/paseo/paseo-daemon.log" mkdir -p "$(dirname "$DAEMON_LOG")" 2>/dev/null || true -# Start the daemon supervisor unless already running. `paseo daemon start -# --foreground` blocks, so the supervisor can see it exit and relaunch it. +# Start the daemon supervisor unless already running. `paseo daemon run` blocks, +# so the supervisor can see it exit and relaunch it. # argv to the inner sh -c: $0=marker, $1=RESTART_DELAY, $2=PASEO_LISTEN. if ! supervisor_running paseo-daemon; then ( sh -c ' while :; do echo "[supervisor] starting paseo daemon at $(date -u +%FT%TZ)" - paseo daemon start --foreground --listen "$2" --web-ui || true + # Let Paseo own its PID lock. It already reclaims dead-owner/stale locks, + # and deleting the lock here could remove a live worker lock during + # restart races. + PASEO_LISTEN="$2" PASEO_WEB_UI_ENABLED=true PASEO_RELAY_ENABLED=false paseo daemon run --home "${PASEO_HOME:-$HOME/.paseo}" || true echo "[supervisor] paseo daemon exited; restarting in ${1}s" sleep "$1" done diff --git a/integrations/isolation/acq-kits/paseo/scripts/verify b/integrations/isolation/acq-kits/paseo/scripts/verify old mode 100644 new mode 100755 index e8591d99..e4859af4 --- a/integrations/isolation/acq-kits/paseo/scripts/verify +++ b/integrations/isolation/acq-kits/paseo/scripts/verify @@ -73,6 +73,7 @@ RUN_SBX="${RUN_SBX:-0}" SBX_KIT_DIR="${SBX_KIT_DIR:-}" # Container port the daemon binds (API + WS + web UI). Keep in sync with spec. PASEO_PORT="${PASEO_PORT:-6767}" +REMOVED_DAEMON_LAUNCH_PATTERN='paseo[[:space:]]+daemon[[:space:]]+start|paseo[[:space:]]+daemon[[:space:]]+(start|run)[^#]*--(listen|web-ui|foreground)' # How long to wait for the background startup, which on FIRST boot also installs # the Paseo CLI (npm). Override with READY_TIMEOUT. READY_TIMEOUT="${READY_TIMEOUT:-120}" @@ -173,15 +174,40 @@ fi # --- Offline check of the mount registrar helper (no sandbox needed). ---------- info "1c. mount registrar helper parses (offline)" +ASSERTCFG="$KIT_DIR/files/home/paseo-assert-daemon-config.mjs" REGMOUNTS="$KIT_DIR/files/home/paseo-register-mounts.mjs" if ! command -v node >/dev/null 2>&1; then - skip "node not on PATH — cannot exercise paseo-register-mounts.mjs" -elif [ ! -f "$REGMOUNTS" ]; then - bad "paseo-register-mounts.mjs missing at $REGMOUNTS" -elif node --check "$REGMOUNTS" >/dev/null 2>&1; then - ok "paseo-register-mounts.mjs parses (node --check)" + skip "node not on PATH — cannot exercise paseo-register-mounts.mjs or paseo-assert-daemon-config.mjs" else - bad "paseo-register-mounts.mjs failed node --check" + if [ ! -f "$ASSERTCFG" ]; then + bad "paseo-assert-daemon-config.mjs missing at $ASSERTCFG" + elif node --check "$ASSERTCFG" >/dev/null 2>&1; then + ok "paseo-assert-daemon-config.mjs parses (node --check)" + else + bad "paseo-assert-daemon-config.mjs failed node --check" + fi + if [ ! -f "$REGMOUNTS" ]; then + bad "paseo-register-mounts.mjs missing at $REGMOUNTS" + elif node --check "$REGMOUNTS" >/dev/null 2>&1; then + ok "paseo-register-mounts.mjs parses (node --check)" + else + bad "paseo-register-mounts.mjs failed node --check" + fi +fi + +# --- Offline check of the daemon launch surface (no sandbox needed). ---------- +info "1d. startup daemon launch surface is 0.9-compatible (offline)" +STARTUP_SCRIPT="$KIT_DIR/files/home/paseo-start.sh" +if [ ! -f "$STARTUP_SCRIPT" ]; then + bad "paseo-start.sh missing at $STARTUP_SCRIPT" +else + removed_flags="$(grep -Ev '^[[:space:]]*#' "$STARTUP_SCRIPT" 2>/dev/null | grep -E "$REMOVED_DAEMON_LAUNCH_PATTERN" || true)" + if [ -n "$removed_flags" ]; then + bad "startup script still references removed Paseo 0.9 daemon launch flags:" + printf '%s\n' "$removed_flags" | sed 's/^/ | /' + else + ok "startup script uses Paseo 0.9-compatible daemon launch surface" + fi fi # --- Select the live driver. ------------------------------------------------- @@ -352,20 +378,34 @@ case "$wrapper_path" in esac [ -n "$(in_sbx 'test -x ~/.local/bin/opencode && echo yes')" ] \ && ok "wrapper file is executable" \ - || bad "~/.local/bin/opencode missing or not executable" + || bad "\$HOME/.local/bin/opencode missing or not executable" info "5. Paseo daemon + web UI (started by the startup supervisor, on its own)" +removed_flags="$(in_sbx "grep -Ev '^[[:space:]]*#' /home/agent/paseo-start.sh 2>/dev/null | grep -E '$REMOVED_DAEMON_LAUNCH_PATTERN' || true")" +if [ -n "$removed_flags" ]; then + bad "startup script still references removed Paseo 0.9 launch flags:" + printf '%s\n' "$removed_flags" | sed 's/^/ | /' +else + ok "startup script uses Paseo 0.9-compatible daemon launch surface" +fi if [ -n "$daemon_up" ]; then ok "daemon answers /api/health on 127.0.0.1:$PASEO_PORT (started by the startup supervisor)" else bad "daemon did not answer /api/health on :$PASEO_PORT (see the daemon log)" in_sbx 'tail -n 20 ~/.local/state/paseo/paseo-daemon.log' | sed 's/^/ | /' fi + +config_assert="$(in_sbx "node /home/agent/paseo-assert-daemon-config.mjs --paseo-home \"\${PASEO_HOME:-\$HOME/.paseo}\" --listen '0.0.0.0:$PASEO_PORT' 2>&1 && echo ok")" +if [ "$config_assert" = "ok" ]; then + ok "config.json persists required daemon settings (listen, web UI, relay disabled)" +else + bad "config.json missing required daemon settings: ${config_assert:-no output}" +fi # The bundled web UI is served on the same origin: the root should return HTML. if [ "$(in_sbx "curl -fsS http://127.0.0.1:$PASEO_PORT/ >/dev/null 2>&1 && echo yes")" = "yes" ]; then ok "bundled web UI is served on the same port ($PASEO_PORT)" elif [ -n "$daemon_up" ]; then - warn "daemon is up but the web UI root did not return 200 (PASEO_WEB_UI_ENABLED / --web-ui?)" + warn "daemon is up but the web UI root did not return 200 (PASEO_WEB_UI_ENABLED / features.webUi.enabled?)" else skip "web UI check (daemon not up)" fi @@ -648,14 +688,14 @@ else fi # (b) daemon self-heal: if the daemon dies, supervisor:paseo-daemon must relaunch -# it. Kill ONLY the daemon tree (argv matches "paseo daemon start" but NOT the +# it. Kill ONLY the daemon tree (argv matches "paseo daemon run" but NOT the # supervisor marker), assert the supervisor survives and the daemon comes back. if [ -z "$daemon_up" ]; then skip "self-heal guard: daemon was not up to kill" else sup_before="$(in_sbx 'pgrep -u "$(id -u)" -f "supervisor:paseo-daemon" | sort | tr "\n" " "')" in_sbx ' - for pid in $(pgrep -u "$(id -u)" -f "paseo daemon start"); do + for pid in $(pgrep -u "$(id -u)" -f "paseo daemon run"); do if tr "\0" " " /dev/null | grep -q "supervisor:paseo-daemon"; then continue fi @@ -714,7 +754,7 @@ else [ -f "$CLIENT" ] || exit 0 CLIENT_URL="file://$CLIENT" node --input-type=module -e " const { connectToDaemon } = await import(process.env.CLIENT_URL); - const c = await connectToDaemon({}); + const c = await connectToDaemon({ target: { kind: \"instance\", home: process.env.PASEO_HOME || (process.env.HOME + \"/.paseo\") } }); try { const r = await c.listProjects(); for (const p of r.projects ?? []) { diff --git a/integrations/isolation/acq-kits/paseo/spec.yaml b/integrations/isolation/acq-kits/paseo/spec.yaml index f0cb0886..65fea9b5 100644 --- a/integrations/isolation/acq-kits/paseo/spec.yaml +++ b/integrations/isolation/acq-kits/paseo/spec.yaml @@ -15,12 +15,12 @@ # # DESIGN (see docs/decisions/): # - The STARTUP script (paseo-start.sh) owns the daemon: on every sandbox start -# it installs/updates the pinned Paseo CLI and supervises the daemon with -# `paseo daemon start --foreground --listen 0.0.0.0:6767 --web-ui`. A single -# Paseo process serves the daemon API, the WebSocket, AND the bundled web UI, -# all on ONE container port (6767). Because a `startup` command fires on EVERY -# start — including a detached `acq create` with nobody attached, held open by -# the sandbox's tini keepalive (PID 1) — the daemon + UI come up on their own. +# it installs/updates the pinned Paseo CLI, persists the daemon settings Paseo +# 0.9 reads from config.json, and supervises `paseo daemon run`. A single Paseo +# process serves the daemon API, the WebSocket, AND the bundled web UI, all on +# ONE container port (6767). Because a `startup` command fires on EVERY start — +# including a detached `acq create` with nobody attached, held open by the +# sandbox's tini keepalive (PID 1) — the daemon + UI come up on their own. # There is deliberately NO install phase (see the install note below). # - The sandbox entrypoint runs the bare command `opencode`. This kit drops a # THIN, agent-named WRAPPER at ~/.local/bin/opencode, which is first on the @@ -63,10 +63,10 @@ # runs). The daemon reads worktrees.root ONCE at boot (no SIGHUP / no runtime # reread — verified against the Paseo source), so the shim — which runs in the # primary-workspace cwd on `acq run` — writes worktrees.root = /.paseo-worktrees -# into config.json and BOUNCES the daemon (kills the supervised child; the -# supervisor respawns it and re-reads config). The supervisor starts the daemon -# immediately with Paseo's default root; the shim rewrites + restarts only when -# the value actually changes. See docs/decisions/worktrees-root-global-only.md and +# into config.json and restarts the daemon worker with `paseo daemon restart`. +# The supervisor starts the daemon immediately with Paseo's default root; the shim +# rewrites + restarts only when the value actually changes. See +# docs/decisions/worktrees-root-global-only.md and # supervisor-owns-daemon-wrapper-pins-worktrees.md. # # PROJECT PRE-POPULATION: at startup (after the daemon answers /api/health) the @@ -152,6 +152,10 @@ files: mode: "0755" source: files/home/paseo-set-worktrees-root.mjs description: Idempotently merge worktrees.root into $PASEO_HOME/config.json (used by the shim) + - path: /home/agent/paseo-assert-daemon-config.mjs + mode: "0755" + source: files/home/paseo-assert-daemon-config.mjs + description: Assert required Paseo daemon settings are persisted before daemon launch - path: /home/agent/paseo-register-mounts.mjs mode: "0755" source: files/home/paseo-register-mounts.mjs @@ -182,7 +186,7 @@ commands: - sh - -c - | - PASEO_CLI_VERSION="0.7.0" \ + PASEO_CLI_VERSION="0.10.2" \ sh /home/agent/paseo-start.sh & # Neutral published port (schema hybrid/v1). Publish the single container/guest @@ -196,10 +200,11 @@ publishedPorts: protocol: tcp name: paseo -# Non-secret guest env. PASEO_WEB_UI_ENABLED enables the bundled UI (equivalent to -# the daemon's --web-ui flag; both are set, harmlessly). PASEO_LISTEN fixes the -# bind address/port. ONNXRUNTIME_NODE_INSTALL=skip mirrors Paseo's own Docker -# image and keeps the voice dep's install from attempting an onnxruntime fetch. +# Non-secret guest env. PASEO_WEB_UI_ENABLED enables the bundled UI on the +# `daemon run` path, and paseo-start.sh also persists the same value into +# config.json for Paseo 0.9 managed/config tooling. PASEO_LISTEN fixes the bind +# address/port. ONNXRUNTIME_NODE_INSTALL=skip mirrors Paseo's own Docker image and +# keeps the voice dep's install from attempting an onnxruntime fetch. # No secret goes here (PASEO_PASSWORD is intentionally unset — unsecured; see # AUTH note above). # @@ -226,9 +231,8 @@ publishedPorts: # daemon logs an endless stream of `relay_error` / `relay_control_disconnected` # (ECONNRESET). The relay is unnecessary here — the host reaches the web UI over # the loopback-published port (see publishedPorts + README "Reaching it from the -# host"), not through Paseo's cloud relay. The env var is read at daemon config -# load (server/config.js) and takes precedence over persisted config, so it holds -# across daemon restarts/self-updates without editing config.json. +# host"), not through Paseo's cloud relay. paseo-start.sh also persists this to +# config.json because Paseo 0.9 managed launches strip daemon-setting env vars. environment: PASEO_WEB_UI_ENABLED: "true" PASEO_LISTEN: "0.0.0.0:6767" diff --git a/integrations/isolation/acq-kits/paseo/tests/paseo-assert-daemon-config.test.mjs b/integrations/isolation/acq-kits/paseo/tests/paseo-assert-daemon-config.test.mjs new file mode 100644 index 00000000..3a495b53 --- /dev/null +++ b/integrations/isolation/acq-kits/paseo/tests/paseo-assert-daemon-config.test.mjs @@ -0,0 +1,51 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { assertDaemonConfig } from "../files/home/paseo-assert-daemon-config.mjs"; + +async function tempDir() { + return mkdtemp(path.join(os.tmpdir(), "paseo-assert-daemon-config-")); +} + +async function writeConfig(paseoHome, config) { + await writeFile(path.join(paseoHome, "config.json"), `${JSON.stringify(config)}\n`); +} + +test("accepts the required daemon settings", async () => { + const paseoHome = await tempDir(); + await writeConfig(paseoHome, { + daemon: { listen: "0.0.0.0:6767", relay: { enabled: false } }, + features: { webUi: { enabled: true } }, + }); + + assert.doesNotThrow(() => assertDaemonConfig({ paseoHome, listen: "0.0.0.0:6767" })); +}); + +test("rejects a missing relay-disable setting", async () => { + const paseoHome = await tempDir(); + await writeConfig(paseoHome, { + daemon: { listen: "0.0.0.0:6767" }, + features: { webUi: { enabled: true } }, + }); + + assert.throws( + () => assertDaemonConfig({ paseoHome, listen: "0.0.0.0:6767" }), + /daemon\.relay\.enabled=undefined/, + ); +}); + +test("rejects wrong listen and disabled web UI", async () => { + const paseoHome = await tempDir(); + await writeConfig(paseoHome, { + daemon: { listen: "127.0.0.1:6767", relay: { enabled: false } }, + features: { webUi: { enabled: false } }, + }); + + assert.throws( + () => assertDaemonConfig({ paseoHome, listen: "0.0.0.0:6767" }), + /daemon\.listen="127\.0\.0\.1:6767".*features\.webUi\.enabled=false/, + ); +}); diff --git a/integrations/isolation/acq-kits/paseo/tests/paseo-set-worktrees-root.test.mjs b/integrations/isolation/acq-kits/paseo/tests/paseo-set-worktrees-root.test.mjs new file mode 100644 index 00000000..708566c6 --- /dev/null +++ b/integrations/isolation/acq-kits/paseo/tests/paseo-set-worktrees-root.test.mjs @@ -0,0 +1,58 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const script = new URL("../files/home/paseo-set-worktrees-root.mjs", import.meta.url); + +async function tempDir() { + return mkdtemp(path.join(os.tmpdir(), "paseo-set-worktrees-root-")); +} + +async function runSetRoot(paseoHome, root, env = {}) { + return execFileAsync(process.execPath, [script.pathname, "--paseo-home", paseoHome, "--root", root], { + env: { ...process.env, ...env }, + timeout: 35_000, + }); +} + +test("sets worktrees.root without clobbering daemon settings", async () => { + const paseoHome = await tempDir(); + await writeFile( + path.join(paseoHome, "config.json"), + JSON.stringify({ + daemon: { listen: "0.0.0.0:6767", relay: { enabled: false } }, + features: { webUi: { enabled: true } }, + }), + ); + const root = path.join(paseoHome, "project", ".paseo-worktrees"); + + const first = await runSetRoot(paseoHome, root); + const second = await runSetRoot(paseoHome, root); + const config = JSON.parse(await readFile(path.join(paseoHome, "config.json"), "utf8")); + + assert.equal(first.stdout, "changed\n"); + assert.equal(second.stdout, "unchanged\n"); + assert.equal(config.worktrees.root, root); + assert.equal(config.daemon.listen, "0.0.0.0:6767"); + assert.equal(config.daemon.relay.enabled, false); + assert.equal(config.features.webUi.enabled, true); +}); + +test("fails closed when the shared config lock is held", async () => { + const paseoHome = await tempDir(); + await mkdir(path.join(paseoHome, ".config-json.lock")); + + await assert.rejects( + runSetRoot(paseoHome, path.join(paseoHome, ".paseo-worktrees"), { + PASEO_CONFIG_LOCK_TIMEOUT_MS: "50", + }), + /cannot lock/, + ); + + await rm(path.join(paseoHome, ".config-json.lock"), { recursive: true, force: true }); +});