From f82e5a71c030448c525b2911d644dab0ba01c9a1 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Fri, 24 Apr 2026 03:08:03 -0300 Subject: [PATCH 01/16] feat(sandbox): add kubernetes runner New Kubernetes runner lives at mesh-plugin-user-sandbox/runner/k8s and sits behind its own subpath export so docker/freestyle deploys never pull in @kubernetes/client-node. Opt in with MESH_SANDBOX_RUNNER=kubernetes; docker stays the dev default. Image side: bumps Bun to 1.3.11 so the daemon can read modern bun.lock (configVersion: 1), and adds a per-boot BOOT_ID to /health so the runner can detect container restarts (OOMKill, eviction) and re-bootstrap the workdir instead of stranding a live pod with an empty /app. --- .../src/api/routes/decopilot/stream-core.ts | 5 +- apps/mesh/src/api/routes/sandbox-daemon.ts | 1 + apps/mesh/src/sandbox/lifecycle.ts | 9 + apps/mesh/src/tools/vm/start.ts | 2 +- apps/mesh/src/web/components/vm/env/env.tsx | 2 +- .../web/components/vm/hooks/use-vm-start.ts | 2 +- bun.lock | 107 ++- .../mesh-plugin-user-sandbox/image/Dockerfile | 6 +- .../mesh-plugin-user-sandbox/image/daemon.mjs | 9 +- .../mesh-plugin-user-sandbox/package.json | 4 + .../server/daemon-client.ts | 21 + .../server/runner/index.ts | 18 +- .../server/runner/k8s/client.test.ts | 409 +++++++++ .../server/runner/k8s/client.ts | 515 +++++++++++ .../server/runner/k8s/constants.ts | 38 + .../server/runner/k8s/index.ts | 19 + .../server/runner/k8s/runner.ts | 812 ++++++++++++++++++ .../server/runner/types.ts | 2 +- packages/mesh-sdk/src/types/virtual-mcp.ts | 4 +- 19 files changed, 1969 insertions(+), 16 deletions(-) create mode 100644 packages/mesh-plugin-user-sandbox/server/runner/k8s/client.test.ts create mode 100644 packages/mesh-plugin-user-sandbox/server/runner/k8s/client.ts create mode 100644 packages/mesh-plugin-user-sandbox/server/runner/k8s/constants.ts create mode 100644 packages/mesh-plugin-user-sandbox/server/runner/k8s/index.ts create mode 100644 packages/mesh-plugin-user-sandbox/server/runner/k8s/runner.ts diff --git a/apps/mesh/src/api/routes/decopilot/stream-core.ts b/apps/mesh/src/api/routes/decopilot/stream-core.ts index b1e3f8a50f..1448ced5db 100644 --- a/apps/mesh/src/api/routes/decopilot/stream-core.ts +++ b/apps/mesh/src/api/routes/decopilot/stream-core.ts @@ -429,7 +429,7 @@ async function streamCoreInner( { vmId: string; previewUrl: string; - runnerKind?: "docker" | "freestyle"; + runnerKind?: "docker" | "freestyle" | "kubernetes"; } > >; @@ -443,7 +443,8 @@ async function streamCoreInner( ? { runnerKind: (activeVmEntry.runnerKind ?? "freestyle") as | "docker" - | "freestyle", + | "freestyle" + | "kubernetes", vmId: activeVmEntry.vmId, } : null; diff --git a/apps/mesh/src/api/routes/sandbox-daemon.ts b/apps/mesh/src/api/routes/sandbox-daemon.ts index 6a59a8c61c..b40298c2f6 100644 --- a/apps/mesh/src/api/routes/sandbox-daemon.ts +++ b/apps/mesh/src/api/routes/sandbox-daemon.ts @@ -16,6 +16,7 @@ import { getRunnerByKind } from "@/sandbox/lifecycle"; const SUPPORTED_KINDS: ReadonlySet = new Set([ "docker", "freestyle", + "kubernetes", ]); async function authorizeSandbox( diff --git a/apps/mesh/src/sandbox/lifecycle.ts b/apps/mesh/src/sandbox/lifecycle.ts index 95ebaf65d2..627030fc7e 100644 --- a/apps/mesh/src/sandbox/lifecycle.ts +++ b/apps/mesh/src/sandbox/lifecycle.ts @@ -34,6 +34,15 @@ async function instantiate( ); return new FreestyleSandboxRunner({ stateStore }); } + case "kubernetes": { + // Dynamic import — @kubernetes/client-node is heavy and only needed + // when MESH_SANDBOX_RUNNER=kubernetes. Docker/Freestyle deploys never + // load it. + const { KubernetesSandboxRunner } = await import( + "mesh-plugin-user-sandbox/runner/k8s" + ); + return new KubernetesSandboxRunner({ stateStore }); + } default: { const exhaustive: never = kind; throw new Error(`Unknown runner kind: ${String(exhaustive)}`); diff --git a/apps/mesh/src/tools/vm/start.ts b/apps/mesh/src/tools/vm/start.ts index 6bbf815378..0558fc2d26 100644 --- a/apps/mesh/src/tools/vm/start.ts +++ b/apps/mesh/src/tools/vm/start.ts @@ -62,7 +62,7 @@ export const VM_START = defineTool({ vmId: z.string(), branch: z.string(), isNewVm: z.boolean(), - runnerKind: z.enum(["docker", "freestyle"]), + runnerKind: z.enum(["docker", "freestyle", "kubernetes"]), }), handler: async (input, ctx) => { diff --git a/apps/mesh/src/web/components/vm/env/env.tsx b/apps/mesh/src/web/components/vm/env/env.tsx index 7b6bcfdf6b..6fb67d3055 100644 --- a/apps/mesh/src/web/components/vm/env/env.tsx +++ b/apps/mesh/src/web/components/vm/env/env.tsx @@ -63,7 +63,7 @@ interface VmData { vmId: string; branch: string; isNewVm: boolean; - runnerKind?: "docker" | "freestyle"; + runnerKind?: "docker" | "freestyle" | "kubernetes"; } type ViewStatus = diff --git a/apps/mesh/src/web/components/vm/hooks/use-vm-start.ts b/apps/mesh/src/web/components/vm/hooks/use-vm-start.ts index a80465d33a..b3f56b392c 100644 --- a/apps/mesh/src/web/components/vm/hooks/use-vm-start.ts +++ b/apps/mesh/src/web/components/vm/hooks/use-vm-start.ts @@ -34,7 +34,7 @@ export interface VmStartResult { vmId: string; branch: string; isNewVm: boolean; - runnerKind?: "docker" | "freestyle"; + runnerKind?: "docker" | "freestyle" | "kubernetes"; } const inflightStarts = new Map>(); diff --git a/bun.lock b/bun.lock index 23e31d058b..1f4d7ab145 100644 --- a/bun.lock +++ b/bun.lock @@ -53,7 +53,7 @@ }, "apps/mesh": { "name": "decocms", - "version": "2.272.5", + "version": "2.272.8", "bin": { "deco": "./dist/server/cli.js", }, @@ -186,6 +186,10 @@ "@freestyle-sh/with-deno": "^0.0.4", "@freestyle-sh/with-nodejs": "^0.2.9", "freestyle-sandboxes": "^0.1.46", + "@freestyle-sh/with-bun": "^0.2.12", + "@freestyle-sh/with-deno": "^0.0.4", + "@freestyle-sh/with-nodejs": "^0.2.9", + "freestyle-sandboxes": "^0.1.46", }, }, "packages/bindings": { @@ -225,10 +229,19 @@ "packages/mesh-plugin-user-sandbox": { "name": "mesh-plugin-user-sandbox", "version": "0.0.1", + "dependencies": { + "@kubernetes/client-node": "^1.4.0", + }, "devDependencies": { "@types/bun": "latest", "typescript": "^5.8.3", }, + "optionalDependencies": { + "@freestyle-sh/with-bun": "^0.2.12", + "@freestyle-sh/with-deno": "^0.0.4", + "@freestyle-sh/with-nodejs": "^0.2.9", + "freestyle-sandboxes": "^0.1.46", + }, }, "packages/mesh-plugin-workflows": { "name": "mesh-plugin-workflows", @@ -883,6 +896,12 @@ "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], + "@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="], + + "@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="], + + "@kubernetes/client-node": ["@kubernetes/client-node@1.4.0", "", { "dependencies": { "@types/js-yaml": "^4.0.1", "@types/node": "^24.0.0", "@types/node-fetch": "^2.6.13", "@types/stream-buffers": "^3.0.3", "form-data": "^4.0.0", "hpagent": "^1.2.0", "isomorphic-ws": "^5.0.0", "js-yaml": "^4.1.0", "jsonpath-plus": "^10.3.0", "node-fetch": "^2.7.0", "openid-client": "^6.1.3", "rfc4648": "^1.3.0", "socks-proxy-agent": "^8.0.4", "stream-buffers": "^3.0.2", "tar-fs": "^3.0.9", "ws": "^8.18.2" } }, "sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA=="], + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="], @@ -1639,6 +1658,8 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], @@ -1661,6 +1682,8 @@ "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -1669,6 +1692,8 @@ "@types/react-syntax-highlighter": ["@types/react-syntax-highlighter@15.5.13", "", { "dependencies": { "@types/react": "*" } }, "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA=="], + "@types/stream-buffers": ["@types/stream-buffers@3.0.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -1755,16 +1780,32 @@ "async-sema": ["async-sema@3.1.1", "", {}, "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + "b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="], + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="], + + "bare-fs": ["bare-fs@4.7.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw=="], + + "bare-os": ["bare-os@3.9.0", "", {}, "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q=="], + + "bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="], + + "bare-stream": ["bare-stream@2.13.0", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA=="], + + "bare-url": ["bare-url@2.4.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A=="], + "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="], @@ -1857,6 +1898,8 @@ "colorjs.io": ["colorjs.io@0.5.2", "", {}, "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "comlink": ["comlink@4.4.2", "", {}, "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], @@ -1953,6 +1996,8 @@ "degit": ["degit@2.8.4", "", { "bin": { "degit": "degit" } }, "sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -2015,6 +2060,8 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -2031,6 +2078,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], @@ -2063,6 +2112,8 @@ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], @@ -2077,6 +2128,8 @@ "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], + "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], @@ -2111,6 +2164,8 @@ "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], @@ -2167,6 +2222,8 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], @@ -2201,6 +2258,8 @@ "hono": ["hono@4.12.7", "", {}, "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw=="], + "hpagent": ["hpagent@1.2.0", "", {}, "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA=="], + "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "html-to-text": ["html-to-text@9.0.5", "", { "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", "dom-serializer": "^2.0.0", "htmlparser2": "^8.0.2", "selderee": "^0.11.0" } }, "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg=="], @@ -2293,6 +2352,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -2305,6 +2366,8 @@ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], @@ -2323,6 +2386,8 @@ "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="], + "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], @@ -2593,6 +2658,8 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "oauth4webapi": ["oauth4webapi@3.8.5", "", {}, "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -2611,6 +2678,8 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "openid-client": ["openid-client@6.8.3", "", { "dependencies": { "jose": "^6.2.2", "oauth4webapi": "^3.8.5" } }, "sha512-AoY/NaN9esS3+xvHInFSK0g3skSfeE0uqQAKRj4rB6/GsBIvzwTUaYo9+HcqpKIaP0dP85p5W07hayKgS4GAeA=="], + "orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], "oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="], @@ -2751,6 +2820,8 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], @@ -2889,6 +2960,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rfc4648": ["rfc4648@1.5.4", "", {}, "sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], @@ -2997,8 +3070,14 @@ "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], "sorted-btree": ["sorted-btree@1.8.1", "", {}, "sha512-395+XIP+wqNn3USkFSrNz7G3Ss/MXlZEqesxvzCRFwL14h6e8LukDHdLBePn5pwbm5OQ9vGu8mDyz2lLDIqamQ=="], @@ -3017,6 +3096,10 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "stream-buffers": ["stream-buffers@3.0.3", "", {}, "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw=="], + + "streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="], + "string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], @@ -3061,8 +3144,16 @@ "tar": ["tar@7.5.11", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ=="], + "tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="], + + "tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="], + + "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], + "terminal-size": ["terminal-size@4.0.1", "", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="], + "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], @@ -3311,6 +3402,7 @@ "@daveyplate/better-auth-ui/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "@decocms/runtime/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@decocms/runtime/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@grpc/proto-loader/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], @@ -3321,6 +3413,8 @@ "@instantdb/react/eventsource": ["eventsource@4.1.0", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ=="], + "@kubernetes/client-node/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "@oclif/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], "@oclif/core/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -3627,6 +3721,8 @@ "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "freestyle-sandboxes/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], "git-diff/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], @@ -3663,14 +3759,18 @@ "mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "mesh-plugin-user-sandbox/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "mesh-plugin-user-sandbox/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "mesh-plugin-workflows/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "mesh-plugin-workflows/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + "openid-client/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "p-queue/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "parse-entities/character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="], @@ -3727,6 +3827,7 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@decocms/runtime/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "@decocms/runtime/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "@oclif/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -3859,6 +3960,8 @@ "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "freestyle-sandboxes/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], "freestyle-sandboxes/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -3887,8 +3990,10 @@ "mdast-util-mdx-jsx/parse-entities/is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "mesh-plugin-user-sandbox/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "mesh-plugin-user-sandbox/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "mesh-plugin-workflows/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "mesh-plugin-workflows/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "shelljs/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], diff --git a/packages/mesh-plugin-user-sandbox/image/Dockerfile b/packages/mesh-plugin-user-sandbox/image/Dockerfile index 1d078b43a9..db7f8aa2a9 100644 --- a/packages/mesh-plugin-user-sandbox/image/Dockerfile +++ b/packages/mesh-plugin-user-sandbox/image/Dockerfile @@ -3,7 +3,11 @@ FROM node:22-bookworm-slim # Bun/Deno versions pinned so a bun.sh / deno.land rebuild can't silently # swap what we ship. Bump intentionally, not by latest-tag drift. -ARG BUN_VERSION=bun-v1.1.42 +# Bun must be >= 1.2 to read the text-format bun.lock (lockfileVersion: 1, +# configVersion: 1) that mesh and other modern monorepos ship. 1.1.x errors +# "Unknown lockfile version" and falls back to lockfile-free resolution, +# which silently skips optional platform binaries and some workspace links. +ARG BUN_VERSION=bun-v1.3.11 ARG DENO_VERSION=v1.46.3 # `locales` + generated `en_US.UTF-8` is load-bearing: repos using diff --git a/packages/mesh-plugin-user-sandbox/image/daemon.mjs b/packages/mesh-plugin-user-sandbox/image/daemon.mjs index e9d606c824..05fa54cbe0 100644 --- a/packages/mesh-plugin-user-sandbox/image/daemon.mjs +++ b/packages/mesh-plugin-user-sandbox/image/daemon.mjs @@ -6,7 +6,14 @@ */ import { spawn } from "node:child_process"; +import { randomUUID } from "node:crypto"; import http from "node:http"; + +// Regenerated per container boot. Mesh persists the last-seen value and +// flips `repoAttached` back to false when it changes — that's how an +// OOMKill/crash restart (which wipes /app under the ephemeral image layer) +// triggers re-bootstrap instead of stranding a live pod with empty workdir. +const BOOT_ID = randomUUID(); import { authorized } from "./daemon/auth.mjs"; import { LOG_RING_CAP, @@ -115,7 +122,7 @@ async function rejectsThreadId(req, res, url) { const server = http.createServer(async (req, res) => { // Unauthenticated: runner probes health before token exists. if (req.method === "GET" && req.url === "/health") { - send(res, 200, { ok: true }); + send(res, 200, { ok: true, bootId: BOOT_ID }); return; } diff --git a/packages/mesh-plugin-user-sandbox/package.json b/packages/mesh-plugin-user-sandbox/package.json index 81d40c249e..ac4b7d8da2 100644 --- a/packages/mesh-plugin-user-sandbox/package.json +++ b/packages/mesh-plugin-user-sandbox/package.json @@ -10,8 +10,12 @@ "exports": { "./shared": "./shared.ts", "./runner": "./server/runner/index.ts", + "./runner/k8s": "./server/runner/k8s/index.ts", "./runner/freestyle": "./server/runner/freestyle/index.ts" }, + "dependencies": { + "@kubernetes/client-node": "^1.4.0" + }, "optionalDependencies": { "@freestyle-sh/with-bun": "^0.2.12", "@freestyle-sh/with-deno": "^0.0.4", diff --git a/packages/mesh-plugin-user-sandbox/server/daemon-client.ts b/packages/mesh-plugin-user-sandbox/server/daemon-client.ts index 76ec052055..f4314c81dc 100644 --- a/packages/mesh-plugin-user-sandbox/server/daemon-client.ts +++ b/packages/mesh-plugin-user-sandbox/server/daemon-client.ts @@ -23,6 +23,27 @@ export async function probeDaemonHealth(daemonUrl: string): Promise { } } +/** + * Reads the daemon's per-boot UUID. Different value between calls means the + * container was restarted (OOMKill, crash, kubelet eviction) and any ephemeral + * workdir state is gone — callers should re-bootstrap. Returns null if the + * daemon is unreachable or predates the bootId field (graceful for rollouts). + */ +export async function readDaemonBootId( + daemonUrl: string, +): Promise { + try { + const res = await fetch(`${daemonUrl}/health`, { + signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS), + }); + if (!res.ok) return null; + const body = (await res.json()) as { bootId?: unknown }; + return typeof body.bootId === "string" ? body.bootId : null; + } catch { + return null; + } +} + /** Polls /health; throws on timeout. Does not stop the container. */ export async function waitForDaemonReady(daemonUrl: string): Promise { for (let i = 0; i < READY_ATTEMPTS; i++) { diff --git a/packages/mesh-plugin-user-sandbox/server/runner/index.ts b/packages/mesh-plugin-user-sandbox/server/runner/index.ts index e9b1281d8a..682c29069b 100644 --- a/packages/mesh-plugin-user-sandbox/server/runner/index.ts +++ b/packages/mesh-plugin-user-sandbox/server/runner/index.ts @@ -1,7 +1,8 @@ /** * Public surface. Ships `DockerSandboxRunner` only via the default entry; - * Freestyle sits behind its own subpath export (./runner/freestyle) because - * its SDK is heavy and not every deploy needs it. + * Freestyle and Kubernetes sit behind their own subpath exports (./runner/ + * freestyle, ./runner/k8s) because their SDKs are heavy and not every deploy + * needs them. */ import { spawnSync } from "node:child_process"; @@ -79,17 +80,22 @@ function isDockerInstalled(): boolean { /** * Rules: - * 1. `MESH_SANDBOX_RUNNER=docker|freestyle` — honored. + * 1. `MESH_SANDBOX_RUNNER=docker|freestyle|kubernetes` — honored. * 2. No explicit value, `FREESTYLE_API_KEY` set — pick freestyle. * 3. Production w/o explicit value and no freestyle key — null. * 4. Dev w/o explicit value — docker if CLI present, else null. + * + * Kubernetes is explicit-only: never auto-selected — callers must opt in + * with `MESH_SANDBOX_RUNNER=kubernetes` so docker-only dev stays the default. */ export function tryResolveRunnerKindFromEnv(): RunnerKind | null { const raw = process.env.MESH_SANDBOX_RUNNER; - if (raw === "docker" || raw === "freestyle") return raw; + if (raw === "docker" || raw === "freestyle" || raw === "kubernetes") { + return raw; + } if (raw && raw.length > 0) { throw new Error( - `Unknown MESH_SANDBOX_RUNNER="${raw}" — expected "docker" or "freestyle".`, + `Unknown MESH_SANDBOX_RUNNER="${raw}" — expected "docker", "freestyle", or "kubernetes".`, ); } if (process.env.FREESTYLE_API_KEY) return "freestyle"; @@ -104,7 +110,7 @@ export function resolveRunnerKindFromEnv(): RunnerKind { if (process.env.NODE_ENV === "production") { throw new Error( `MESH_SANDBOX_RUNNER must be set explicitly in production — ` + - `choose "docker" or "freestyle" (or set FREESTYLE_API_KEY).`, + `choose "docker", "freestyle", or "kubernetes" (or set FREESTYLE_API_KEY).`, ); } throw new Error( diff --git a/packages/mesh-plugin-user-sandbox/server/runner/k8s/client.test.ts b/packages/mesh-plugin-user-sandbox/server/runner/k8s/client.test.ts new file mode 100644 index 0000000000..b05cf16b76 --- /dev/null +++ b/packages/mesh-plugin-user-sandbox/server/runner/k8s/client.test.ts @@ -0,0 +1,409 @@ +import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; +import { K8S_CONSTANTS } from "./constants"; +import { + createSandboxClaim, + deleteSandboxClaim, + getSandboxClaim, + patchSandboxClaimShutdown, + type SandboxClaim, + type SandboxResource, + waitForSandboxReady, +} from "./client"; + +// ---- Minimal KubeConfig stub ----------------------------------------------- +// client.ts only touches `getCurrentCluster` and `applyToHTTPSOptions`; the +// stub mirrors those and omits the 100-method surface of the real class. + +const STUB_SERVER = "https://kube.test"; + +function makeKc( + cluster: { server: string; skipTLSVerify?: boolean } = { + server: STUB_SERVER, + }, +) { + const apply = async (opts: Record) => { + opts.headers = { Authorization: "Bearer stub-token" }; + opts.cert = "STUB_CERT_PEM"; + opts.key = "STUB_KEY_PEM"; + opts.ca = "STUB_CA_PEM"; + }; + return { + getCurrentCluster: () => cluster, + applyToHTTPSOptions: apply, + } as unknown as import("@kubernetes/client-node").KubeConfig; +} + +// ---- Fetch interception ---------------------------------------------------- +// Keep the real global fetch so test infra (bun itself) isn't affected, but +// swap it per-test with a stub that records calls + returns scripted responses. + +type FetchCall = { url: string; init: RequestInit }; +const fetchCalls: FetchCall[] = []; +let fetchImpl: (url: string, init: RequestInit) => Promise = + async () => { + throw new Error("no fetch impl set"); + }; +const originalFetch = globalThis.fetch; + +beforeEach(() => { + fetchCalls.length = 0; + globalThis.fetch = mock(async (url: URL | string, init: RequestInit = {}) => { + const record: FetchCall = { + url: typeof url === "string" ? url : url.toString(), + init, + }; + fetchCalls.push(record); + return fetchImpl(record.url, init); + }) as unknown as typeof globalThis.fetch; +}); + +afterEach(() => { + globalThis.fetch = originalFetch; +}); + +// ---- Response helpers ------------------------------------------------------- + +function jsonResponse(status: number, body: unknown): Response { + return new Response(body === undefined ? null : JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); +} + +/** Build a response whose body is a push-driven ND-JSON stream. */ +function ndJsonResponse(status: number): { + resp: Response; + push: (obj: unknown) => void; + close: () => void; +} { + let controller!: ReadableStreamDefaultController; + const stream = new ReadableStream({ + start: (c) => { + controller = c; + }, + }); + const encoder = new TextEncoder(); + return { + resp: new Response(stream, { + status, + headers: { "content-type": "application/json" }, + }), + push: (obj) => + controller.enqueue(encoder.encode(`${JSON.stringify(obj)}\n`)), + close: () => controller.close(), + }; +} + +// ---- Fixtures --------------------------------------------------------------- + +const NS = "agent-sandbox-system"; + +function makeClaim(name: string): SandboxClaim { + return { + apiVersion: `${K8S_CONSTANTS.CLAIM_API_GROUP}/${K8S_CONSTANTS.CLAIM_API_VERSION}`, + kind: "SandboxClaim", + metadata: { name, namespace: NS }, + spec: { + sandboxTemplateRef: { name: "mesh-sandbox" }, + lifecycle: { shutdownPolicy: "Delete" }, + }, + }; +} + +// ---------------------------------------------------------------------------- + +describe("createSandboxClaim", () => { + it("POSTs the claim body verbatim to the plural endpoint", async () => { + fetchImpl = async () => jsonResponse(201, { kind: "SandboxClaim" }); + const claim = makeClaim("mesh-sb-abc"); + await createSandboxClaim(makeKc(), NS, claim); + + expect(fetchCalls).toHaveLength(1); + const [call] = fetchCalls; + expect(call!.url).toBe( + `${STUB_SERVER}/apis/${K8S_CONSTANTS.CLAIM_API_GROUP}/${K8S_CONSTANTS.CLAIM_API_VERSION}/namespaces/${NS}/${K8S_CONSTANTS.CLAIM_PLURAL}`, + ); + expect(call!.init.method).toBe("POST"); + expect(JSON.parse(String(call!.init.body))).toEqual(claim); + // Auth header flows through from applyToHTTPSOptions. + const headers = call!.init.headers as Record; + expect(headers.Authorization).toBe("Bearer stub-token"); + }); + + it("round-trips spec.env + warmpool (per-claim DAEMON_TOKEN shape)", async () => { + // Stage 2.1 claim shape: per-claim env requires warmpool: "none". + // Lock the exact wire payload so a bad serializer regression (dropping + // env, mangling warmpool) surfaces in unit tests — before it wastes a + // kind-cluster provision cycle discovering the same bug. + fetchImpl = async () => jsonResponse(201, { kind: "SandboxClaim" }); + const claim: SandboxClaim = { + apiVersion: `${K8S_CONSTANTS.CLAIM_API_GROUP}/${K8S_CONSTANTS.CLAIM_API_VERSION}`, + kind: "SandboxClaim", + metadata: { name: "mesh-sb-tok", namespace: NS }, + spec: { + sandboxTemplateRef: { name: "mesh-sandbox" }, + env: [{ name: "DAEMON_TOKEN", value: "abc123" }], + warmpool: "none", + lifecycle: { shutdownPolicy: "Delete" }, + }, + }; + await createSandboxClaim(makeKc(), NS, claim); + const body = JSON.parse(String(fetchCalls[0]!.init.body)); + expect(body.spec.env).toEqual([{ name: "DAEMON_TOKEN", value: "abc123" }]); + expect(body.spec.warmpool).toBe("none"); + }); + + it("wraps non-2xx errors in SandboxError with the claim name", async () => { + fetchImpl = async () => + jsonResponse(409, { + kind: "Status", + status: "Failure", + reason: "AlreadyExists", + message: "already exists", + code: 409, + }); + await expect( + createSandboxClaim(makeKc(), NS, makeClaim("dup")), + ).rejects.toThrow(/Failed to create SandboxClaim: dup/); + }); +}); + +describe("deleteSandboxClaim", () => { + it("swallows 404 silently (idempotent delete)", async () => { + fetchImpl = async () => + jsonResponse(404, { + kind: "Status", + reason: "NotFound", + message: "not found", + }); + await expect( + deleteSandboxClaim(makeKc(), NS, "gone"), + ).resolves.toBeUndefined(); + expect(fetchCalls[0]!.init.method).toBe("DELETE"); + }); + + it("re-throws non-404 errors wrapped in SandboxError", async () => { + fetchImpl = async () => + jsonResponse(403, { + kind: "Status", + reason: "Forbidden", + message: "forbidden", + }); + await expect(deleteSandboxClaim(makeKc(), NS, "x")).rejects.toThrow( + /Failed to delete SandboxClaim: x/, + ); + }); +}); + +describe("getSandboxClaim", () => { + it("returns undefined on 404", async () => { + fetchImpl = async () => + jsonResponse(404, { + kind: "Status", + reason: "NotFound", + message: "not found", + }); + const result = await getSandboxClaim(makeKc(), NS, "missing"); + expect(result).toBeUndefined(); + }); + + it("returns the resource body on 200", async () => { + const body: SandboxResource = { + metadata: { name: "present" }, + status: { conditions: [{ type: "Ready", status: "False" }] }, + }; + fetchImpl = async () => jsonResponse(200, body); + const result = await getSandboxClaim(makeKc(), NS, "present"); + expect(result).toEqual(body); + }); + + it("URL-encodes the claim name", async () => { + fetchImpl = async () => jsonResponse(404, null); + await getSandboxClaim(makeKc(), NS, "weird/name"); + expect(fetchCalls[0]!.url).toContain("/weird%2Fname"); + }); +}); + +describe("patchSandboxClaimShutdown", () => { + it("sends merge-patch with lifecycle.shutdownTime only", async () => { + fetchImpl = async () => jsonResponse(200, { kind: "SandboxClaim" }); + await patchSandboxClaimShutdown( + makeKc(), + NS, + "mesh-sb-x", + "2026-04-01T12:00:00.000Z", + ); + expect(fetchCalls).toHaveLength(1); + const [call] = fetchCalls; + expect(call!.init.method).toBe("PATCH"); + const headers = call!.init.headers as Record; + expect(headers["content-type"]).toBe("application/merge-patch+json"); + expect(JSON.parse(String(call!.init.body))).toEqual({ + spec: { + lifecycle: { + shutdownPolicy: "Delete", + shutdownTime: "2026-04-01T12:00:00.000Z", + }, + }, + }); + }); + + it("swallows 404 silently (claim deleted between lookup and patch)", async () => { + fetchImpl = async () => + jsonResponse(404, { + kind: "Status", + reason: "NotFound", + message: "not found", + }); + await expect( + patchSandboxClaimShutdown( + makeKc(), + NS, + "gone", + "2026-04-01T12:00:00.000Z", + ), + ).resolves.toBeUndefined(); + }); + + it("wraps other errors in SandboxError", async () => { + fetchImpl = async () => + jsonResponse(409, { + kind: "Status", + reason: "Conflict", + message: "conflict", + }); + await expect( + patchSandboxClaimShutdown( + makeKc(), + NS, + "busy", + "2026-04-01T12:00:00.000Z", + ), + ).rejects.toThrow(/Failed to patch SandboxClaim shutdownTime: busy/); + }); +}); + +describe("waitForSandboxReady", () => { + it("resolves with sandboxName + podName once Ready=True is observed", async () => { + const stream = ndJsonResponse(200); + fetchImpl = async () => stream.resp; + const p = waitForSandboxReady(makeKc(), NS, "claim-xyz", 60); + stream.push({ + type: "MODIFIED", + object: { + metadata: { + name: "claim-xyz", + annotations: { [K8S_CONSTANTS.POD_NAME_ANNOTATION]: "pod-42" }, + }, + status: { conditions: [{ type: "Ready", status: "True" }] }, + }, + }); + await expect(p).resolves.toEqual({ + sandboxName: "claim-xyz", + podName: "pod-42", + }); + const url = fetchCalls[0]!.url; + expect(url).toContain("?watch=true"); + expect(url).toContain("fieldSelector="); + }); + + it("falls back to sandboxName when pod-name annotation is absent", async () => { + const stream = ndJsonResponse(200); + fetchImpl = async () => stream.resp; + const p = waitForSandboxReady(makeKc(), NS, "claim-xyz", 60); + stream.push({ + type: "MODIFIED", + object: { + metadata: { name: "claim-xyz" }, + status: { conditions: [{ type: "Ready", status: "True" }] }, + }, + }); + await expect(p).resolves.toEqual({ + sandboxName: "claim-xyz", + podName: "claim-xyz", + }); + }); + + it("ignores non-Ready conditions and keeps watching", async () => { + const stream = ndJsonResponse(200); + fetchImpl = async () => stream.resp; + const p = waitForSandboxReady(makeKc(), NS, "claim-xyz", 60); + // Emit a non-Ready condition — should not settle. + stream.push({ + type: "MODIFIED", + object: { + metadata: { name: "claim-xyz" }, + status: { conditions: [{ type: "Progressing", status: "True" }] }, + }, + }); + const sentinel = Symbol("still-pending"); + const winner = await Promise.race([ + p, + new Promise((r) => setTimeout(() => r(sentinel), 10)), + ]); + expect(winner).toBe(sentinel); + + stream.push({ + type: "MODIFIED", + object: { + metadata: { name: "claim-xyz" }, + status: { conditions: [{ type: "Ready", status: "True" }] }, + }, + }); + await expect(p).resolves.toEqual({ + sandboxName: "claim-xyz", + podName: "claim-xyz", + }); + }); + + it("rejects with SandboxTimeoutError after the deadline", async () => { + // Server accepts the connection but never emits — simulate a watch that + // just hangs. 0-second timeout fires on the next tick. + const stream = ndJsonResponse(200); + fetchImpl = async () => stream.resp; + const p = waitForSandboxReady(makeKc(), NS, "claim-xyz", 0); + await expect(p).rejects.toThrow(/did not become ready within 0 seconds/); + }); + + it("rejects if the watch handshake itself fails", async () => { + fetchImpl = async () => { + throw new Error("kube-apiserver unreachable"); + }; + const p = waitForSandboxReady(makeKc(), NS, "claim-xyz", 60); + await expect(p).rejects.toThrow( + /Failed to start watch for sandbox readiness/, + ); + }); + + it("rejects when the Sandbox object has no metadata.name", async () => { + const stream = ndJsonResponse(200); + fetchImpl = async () => stream.resp; + const p = waitForSandboxReady(makeKc(), NS, "claim-xyz", 60); + stream.push({ + type: "MODIFIED", + object: { + // no metadata.name + status: { conditions: [{ type: "Ready", status: "True" }] }, + }, + }); + await expect(p).rejects.toThrow(/Sandbox metadata or name is missing/); + }); + + it("rejects on ERROR frames from the watch stream", async () => { + const stream = ndJsonResponse(200); + fetchImpl = async () => stream.resp; + const p = waitForSandboxReady(makeKc(), NS, "claim-xyz", 60); + stream.push({ + type: "ERROR", + object: { + kind: "Status", + status: "Failure", + reason: "Expired", + message: "watch channel expired", + }, + }); + await expect(p).rejects.toThrow( + /Watch stream error while waiting for sandbox: watch channel expired/, + ); + }); +}); diff --git a/packages/mesh-plugin-user-sandbox/server/runner/k8s/client.ts b/packages/mesh-plugin-user-sandbox/server/runner/k8s/client.ts new file mode 100644 index 0000000000..3289b9f69c --- /dev/null +++ b/packages/mesh-plugin-user-sandbox/server/runner/k8s/client.ts @@ -0,0 +1,515 @@ +/** + * Low-level CRUD + readiness watch for agent-sandbox SandboxClaim / Sandbox. + * + * Talks to the k8s REST API directly via the runtime's native `fetch` with + * `{ tls: { cert, key, ca } }`. Credentials (client cert + CA) are extracted + * from the active `KubeConfig` context using the library's own + * `applyToHTTPSOptions` helper. + * + * Why not `kc.makeApiClient(CustomObjectsApi)` like admin does: + * `@kubernetes/client-node` 1.x's generated clients ship an + * `IsomorphicFetchHttpLibrary` that calls `fetch(url, { agent })` — a + * node-fetch signal that Node's https.Agent (cert/key/ca) should be used + * for the TLS handshake. Bun's node-fetch polyfill silently drops the + * `agent` option: TLS verification fails and, if bypassed, the cluster + * sees `system:anonymous` (no client cert). The fix is Bun-native: + * `fetch(url, { tls: { cert, key, ca } })`. The library's Watch hits + * the same bug, so readiness is rebuilt from scratch here too. + * + * Surface intentionally minimal: create/delete/get/waitForReady. Higher-level + * "ensure ready" flows live on the runner, not here. + */ + +import { + type KubeConfig, + type V1Status as V1StatusUpstream, +} from "@kubernetes/client-node"; +import { K8S_CONSTANTS, SandboxError, SandboxTimeoutError } from "./constants"; + +type V1Status = Partial & { reason?: string }; + +/** + * Subset of SandboxClaim `spec.env[]`. The CRD accepts only literal + * `{name, value}` pairs — no `valueFrom`/`secretKeyRef`. That's why Stage 2.1 + * injects `DAEMON_TOKEN` here directly rather than via a Secret reference. + */ +export interface SandboxClaimEnvVar { + name: string; + value: string; + containerName?: string; +} + +export interface SandboxClaim { + apiVersion: string; + kind: "SandboxClaim"; + metadata: { + name: string; + namespace?: string; + labels?: Record; + annotations?: Record; + }; + spec: { + sandboxTemplateRef: { name: string }; + env?: SandboxClaimEnvVar[]; + /** + * `"none"` forces a fresh pod per claim — required when `spec.env` is + * set because the operator rejects custom env when the claim would + * come from a warm pool (warm pods are pre-started, can't take new + * env). Passing `undefined` lets the CRD default ("default") apply. + */ + warmpool?: "none" | "default" | string; + lifecycle?: { + shutdownTime?: string; + shutdownPolicy?: "Delete" | "Retain"; + }; + }; +} + +export interface SandboxCondition { + type: string; + status: string; + reason?: string; + message?: string; +} + +export interface SandboxResource { + metadata?: { + name?: string; + labels?: Record; + annotations?: Record; + }; + /** + * Present when this came back from `getSandboxClaim` (CRD has a spec); + * absent from Sandbox-kind resources because `waitForSandboxReady` only + * projects out metadata/status. `adopt()` reads `spec.env` to recover the + * per-claim DAEMON_TOKEN it originally injected. + */ + spec?: { + sandboxTemplateRef?: { name?: string }; + env?: SandboxClaimEnvVar[]; + lifecycle?: { + shutdownTime?: string; + shutdownPolicy?: "Delete" | "Retain"; + }; + }; + status?: { + conditions?: SandboxCondition[]; + }; +} + +type WatchEvent = { + type: "ADDED" | "MODIFIED" | "DELETED" | "BOOKMARK" | "ERROR"; + object: SandboxResource | V1Status; +}; + +// ---- Transport -------------------------------------------------------------- + +/** Resolved auth + TLS material for the active kubeconfig context. */ +interface KubeAuth { + server: string; + headers: Record; + tls: { + cert?: string; + key?: string; + ca?: string; + rejectUnauthorized?: boolean; + }; +} + +async function resolveKubeAuth(kc: KubeConfig): Promise { + const cluster = kc.getCurrentCluster(); + if (!cluster) throw new SandboxError("No active cluster in kubeconfig"); + + // `applyToHTTPSOptions` mutates a plain options object, threading through the + // authenticator chain (token files, exec plugins, etc.). We harvest the bits + // we care about — headers (bearer/impersonation), cert/key/ca — and discard + // the `agent` it leaves behind since we route around node-fetch entirely. + const opts: Record = {}; + await kc.applyToHTTPSOptions(opts); + + const headers: Record = {}; + const optHeaders = (opts.headers ?? {}) as Record; + for (const [k, v] of Object.entries(optHeaders)) { + if (Array.isArray(v)) headers[k] = v.join(", "); + else if (v !== undefined) headers[k] = String(v); + } + if (typeof opts.auth === "string" && !headers.Authorization) { + headers.Authorization = `Basic ${Buffer.from(opts.auth).toString("base64")}`; + } + + return { + server: cluster.server.replace(/\/+$/, ""), + headers, + tls: { + cert: bufferLike(opts.cert), + key: bufferLike(opts.key), + ca: bufferLike(opts.ca), + rejectUnauthorized: cluster.skipTLSVerify ? false : undefined, + }, + }; +} + +function bufferLike(v: unknown): string | undefined { + if (v == null) return undefined; + if (typeof v === "string") return v; + if (Buffer.isBuffer(v)) return v.toString("utf8"); + return String(v); +} + +interface KubeFetchInit { + method: "GET" | "POST" | "DELETE"; + path: string; + body?: unknown; + signal?: AbortSignal; + /** Extra Accept / query hints. Merged with auth headers. */ + headers?: Record; +} + +/** + * Thin wrapper around `fetch` that threads TLS + auth from the kubeconfig. + * Returns the raw `Response` so streaming callers (watch) can consume the + * body themselves; non-streaming callers parse JSON explicitly. + */ +async function kubeFetch( + kc: KubeConfig, + init: KubeFetchInit, +): Promise { + const auth = await resolveKubeAuth(kc); + const url = `${auth.server}${init.path}`; + const headers: Record = { ...auth.headers, ...init.headers }; + if (init.body !== undefined && !("content-type" in headers)) { + headers["content-type"] = "application/json"; + } + const reqInit: RequestInit & { tls?: typeof auth.tls } = { + method: init.method, + headers, + body: init.body === undefined ? undefined : JSON.stringify(init.body), + signal: init.signal, + tls: auth.tls, + }; + return fetch(url, reqInit as RequestInit); +} + +interface KubeFetchInitWithPatch extends Omit { + method: "PATCH"; + patchType: "merge" | "strategic-merge"; +} + +async function kubePatch( + kc: KubeConfig, + init: KubeFetchInitWithPatch, +): Promise { + const auth = await resolveKubeAuth(kc); + const url = `${auth.server}${init.path}`; + const contentType = + init.patchType === "strategic-merge" + ? "application/strategic-merge-patch+json" + : "application/merge-patch+json"; + const reqInit: RequestInit & { tls?: typeof auth.tls } = { + method: "PATCH", + headers: { ...auth.headers, "content-type": contentType }, + body: init.body === undefined ? undefined : JSON.stringify(init.body), + signal: init.signal, + tls: auth.tls, + }; + return fetch(url, reqInit as RequestInit); +} + +/** HTTP error carrier used for the 404 fast-path before SandboxError wrapping. */ +class KubeHttpError extends Error { + constructor( + readonly status: number, + readonly body: V1Status | null, + message: string, + ) { + super(message); + this.name = "KubeHttpError"; + } +} + +async function readStatusBody(resp: Response): Promise { + try { + return (await resp.json()) as V1Status; + } catch { + return null; + } +} + +async function ensureOk(resp: Response, action: string): Promise { + if (resp.ok) return; + const body = await readStatusBody(resp); + const message = + body?.message ?? `${action} failed: ${resp.status} ${resp.statusText}`; + throw new KubeHttpError(resp.status, body, message); +} + +// ---- Public surface --------------------------------------------------------- + +const CLAIM_PATH_PREFIX = `/apis/${K8S_CONSTANTS.CLAIM_API_GROUP}/${K8S_CONSTANTS.CLAIM_API_VERSION}/namespaces`; + +export async function createSandboxClaim( + kc: KubeConfig, + namespace: string, + claim: SandboxClaim, +): Promise { + const path = `${CLAIM_PATH_PREFIX}/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.CLAIM_PLURAL}`; + try { + const resp = await kubeFetch(kc, { method: "POST", path, body: claim }); + await ensureOk(resp, "createSandboxClaim"); + } catch (error) { + throw new SandboxError( + `Failed to create SandboxClaim: ${claim.metadata.name}`, + error, + ); + } +} + +/** + * Update the claim's idle-reap clock. The agent-sandbox operator honors + * `spec.lifecycle.shutdownTime` with `shutdownPolicy: Delete`: once the + * wall clock passes `shutdownTime`, the operator deletes the claim + pod. + * + * Mesh calls this on every `ensure()` hit so an active sandbox continuously + * pushes its deadline forward; an abandoned one hits the deadline and the + * operator reaps it. No mesh-side cron/reconcile needed. + * + * Uses merge-patch (RFC 7396), which is the documented patch format for + * CRDs — strategic-merge only works on built-in types that ship merge + * keys. 404 is swallowed because a deleted-since-lookup claim is not an + * error from mesh's perspective; the caller's next ensure() will + * re-provision. + */ +export async function patchSandboxClaimShutdown( + kc: KubeConfig, + namespace: string, + claimName: string, + shutdownTime: string, +): Promise { + const path = `${CLAIM_PATH_PREFIX}/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.CLAIM_PLURAL}/${encodeURIComponent(claimName)}`; + try { + const resp = await kubePatch(kc, { + method: "PATCH", + path, + patchType: "merge", + body: { + spec: { + lifecycle: { + shutdownPolicy: "Delete", + shutdownTime, + }, + }, + }, + }); + if (resp.status === 404) return; + await ensureOk(resp, "patchSandboxClaimShutdown"); + } catch (error) { + if (error instanceof KubeHttpError && error.status === 404) return; + throw new SandboxError( + `Failed to patch SandboxClaim shutdownTime: ${claimName}`, + error, + ); + } +} + +export async function deleteSandboxClaim( + kc: KubeConfig, + namespace: string, + claimName: string, +): Promise { + const path = `${CLAIM_PATH_PREFIX}/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.CLAIM_PLURAL}/${encodeURIComponent(claimName)}`; + try { + const resp = await kubeFetch(kc, { method: "DELETE", path }); + if (resp.status === 404) return; + await ensureOk(resp, "deleteSandboxClaim"); + } catch (error) { + if (error instanceof KubeHttpError && error.status === 404) return; + throw new SandboxError( + `Failed to delete SandboxClaim: ${claimName}`, + error, + ); + } +} + +export async function getSandboxClaim( + kc: KubeConfig, + namespace: string, + claimName: string, +): Promise { + const path = `${CLAIM_PATH_PREFIX}/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.CLAIM_PLURAL}/${encodeURIComponent(claimName)}`; + try { + const resp = await kubeFetch(kc, { method: "GET", path }); + if (resp.status === 404) return undefined; + await ensureOk(resp, "getSandboxClaim"); + return (await resp.json()) as SandboxResource; + } catch (error) { + if (error instanceof KubeHttpError && error.status === 404) + return undefined; + throw new SandboxError(`Failed to get SandboxClaim: ${claimName}`, error); + } +} + +export interface WaitForSandboxReadyResult { + sandboxName: string; + podName: string; +} + +/** + * Resolves on the first `Ready=True` condition on the Sandbox matching + * `claimName`; rejects on stream error, missing name metadata, or timeout. + * The watch is aborted exactly once via `settle()`; callers get deterministic + * teardown regardless of which branch fires first. + */ +export function waitForSandboxReady( + kc: KubeConfig, + namespace: string, + claimName: string, + timeoutSeconds = 180, +): Promise { + const path = `/apis/${K8S_CONSTANTS.SANDBOX_API_GROUP}/${K8S_CONSTANTS.SANDBOX_API_VERSION}/namespaces/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.SANDBOX_PLURAL}?watch=true&fieldSelector=${encodeURIComponent(`metadata.name=${claimName}`)}`; + + const { resolve, reject, promise } = + Promise.withResolvers(); + + const controller = new AbortController(); + let settled = false; + const timeoutHandle = setTimeout(() => { + if (settled) return; + settled = true; + controller.abort(); + reject( + new SandboxTimeoutError( + `Sandbox did not become ready within ${timeoutSeconds} seconds`, + ), + ); + }, timeoutSeconds * 1000); + + const settleWith = (fn: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeoutHandle); + controller.abort(); + fn(); + }; + + (async () => { + let resp: Response; + try { + resp = await kubeFetch(kc, { + method: "GET", + path, + signal: controller.signal, + headers: { accept: "application/json" }, + }); + } catch (err) { + settleWith(() => + reject( + new SandboxError("Failed to start watch for sandbox readiness", err), + ), + ); + return; + } + + if (!resp.ok || !resp.body) { + const body = await readStatusBody(resp).catch(() => null); + settleWith(() => + reject( + new SandboxError( + `Watch handshake failed (${resp.status}): ${body?.message ?? resp.statusText}`, + ), + ), + ); + return; + } + + try { + for await (const event of readNdJson(resp.body)) { + if (settled) return; + // Bookmark/ERROR/DELETED are never a "ready" signal. ERROR carries a + // V1Status payload rather than a SandboxResource; treating it as a + // fatal stream error mirrors client-go's behaviour. + if (event.type === "ERROR") { + const status = event.object as V1Status; + settleWith(() => + reject( + new SandboxError( + `Watch stream error while waiting for sandbox: ${status.message ?? "unknown"}`, + ), + ), + ); + return; + } + if (event.type !== "ADDED" && event.type !== "MODIFIED") continue; + + const sandbox = event.object as SandboxResource; + const ready = sandbox.status?.conditions?.find( + (c) => c.type === "Ready" && c.status === "True", + ); + if (!ready) continue; + + const sandboxName = sandbox.metadata?.name; + if (!sandboxName) { + settleWith(() => + reject(new SandboxError("Sandbox metadata or name is missing")), + ); + return; + } + const podName = + sandbox.metadata?.annotations?.[K8S_CONSTANTS.POD_NAME_ANNOTATION] ?? + sandboxName; + settleWith(() => resolve({ sandboxName, podName })); + return; + } + // Stream ended before Ready observed — treat as transient failure so the + // caller can retry rather than wait out the timeout. + settleWith(() => + reject( + new SandboxError("Watch stream closed before sandbox became ready"), + ), + ); + } catch (err) { + if (settled) return; + // AbortError during in-flight stream is the timeout path above; don't + // double-reject. + if ( + err instanceof Error && + (err.name === "AbortError" || controller.signal.aborted) + ) + return; + settleWith(() => + reject( + new SandboxError("Watch stream error while waiting for sandbox", err), + ), + ); + } + })(); + + return promise; +} + +/** ND-JSON line reader over a WHATWG ReadableStream. */ +async function* readNdJson( + stream: ReadableStream, +): AsyncGenerator { + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let buf = ""; + try { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + buf += decoder.decode(value, { stream: true }); + let newline: number; + // biome-ignore lint/suspicious/noAssignInExpressions: idiomatic line loop + while ((newline = buf.indexOf("\n")) >= 0) { + const line = buf.slice(0, newline).trim(); + buf = buf.slice(newline + 1); + if (!line) continue; + yield JSON.parse(line) as T; + } + } + const tail = buf.trim(); + if (tail) yield JSON.parse(tail) as T; + } finally { + reader.releaseLock(); + } +} diff --git a/packages/mesh-plugin-user-sandbox/server/runner/k8s/constants.ts b/packages/mesh-plugin-user-sandbox/server/runner/k8s/constants.ts new file mode 100644 index 0000000000..8ee6d68bb6 --- /dev/null +++ b/packages/mesh-plugin-user-sandbox/server/runner/k8s/constants.ts @@ -0,0 +1,38 @@ +/** + * agent-sandbox CRD identifiers + error classes. Pinned verbatim from + * kubernetes-sigs/agent-sandbox via deco-cx/admin/clients/agent-sandbox/types.ts. + * When the operator widens to a new API version, change here once — every + * call site reads through these constants. + */ + +export const K8S_CONSTANTS = { + CLAIM_API_GROUP: "extensions.agents.x-k8s.io", + CLAIM_API_VERSION: "v1alpha1", + CLAIM_PLURAL: "sandboxclaims", + + SANDBOX_API_GROUP: "agents.x-k8s.io", + SANDBOX_API_VERSION: "v1alpha1", + SANDBOX_PLURAL: "sandboxes", + + POD_NAME_ANNOTATION: "agents.x-k8s.io/pod-name", +} as const; + +export class SandboxError extends Error { + override readonly cause?: unknown; + + constructor(message: string, cause?: unknown) { + super(message); + this.name = "SandboxError"; + this.cause = cause; + if (cause instanceof Error && cause.stack) { + this.stack = `${this.stack}\nCaused by: ${cause.stack}`; + } + } +} + +export class SandboxTimeoutError extends SandboxError { + constructor(message: string, cause?: unknown) { + super(message, cause); + this.name = "SandboxTimeoutError"; + } +} diff --git a/packages/mesh-plugin-user-sandbox/server/runner/k8s/index.ts b/packages/mesh-plugin-user-sandbox/server/runner/k8s/index.ts new file mode 100644 index 0000000000..3e63c7c75e --- /dev/null +++ b/packages/mesh-plugin-user-sandbox/server/runner/k8s/index.ts @@ -0,0 +1,19 @@ +// Re-exported so external tooling (e.g. deploy/k8s-sandbox/local/smoke.ts) +// can build a KubeConfig without declaring @kubernetes/client-node itself. +export { KubeConfig } from "@kubernetes/client-node"; +export { K8S_CONSTANTS, SandboxError, SandboxTimeoutError } from "./constants"; +export { + createSandboxClaim, + deleteSandboxClaim, + getSandboxClaim, + waitForSandboxReady, +} from "./client"; +export type { + SandboxClaim, + SandboxClaimEnvVar, + SandboxCondition, + SandboxResource, + WaitForSandboxReadyResult, +} from "./client"; +export { KubernetesSandboxRunner } from "./runner"; +export type { KubernetesRunnerOptions } from "./runner"; diff --git a/packages/mesh-plugin-user-sandbox/server/runner/k8s/runner.ts b/packages/mesh-plugin-user-sandbox/server/runner/k8s/runner.ts new file mode 100644 index 0000000000..f7a3b4a061 --- /dev/null +++ b/packages/mesh-plugin-user-sandbox/server/runner/k8s/runner.ts @@ -0,0 +1,812 @@ +/** + * Kubernetes sandbox runner. + * + * Provisions one SandboxClaim per (user, projectRef) against the + * kubernetes-sigs/agent-sandbox operator. Mesh runs outside the cluster + * (Stage 1 / local-dev via kind), so daemon and dev traffic both reach the + * pod via a lazily-opened 127.0.0.1 TCP listener that tunnels each inbound + * connection through the apiserver as a fresh WebSocket. + * + * Stage 3 replaces the port-forward path with real ingress: when + * `previewUrlPattern` is set, no dev port-forward is opened and the preview + * URL is synthesized from the handle. + * + * Token model: each claim carries a freshly-generated DAEMON_TOKEN injected + * via `SandboxClaim.spec.env`. One leak compromises one sandbox. + * `valueFrom.secretKeyRef` isn't supported on SandboxClaim env; RBAC on + * the namespace is the secrecy boundary. + */ + +import { createHash, randomBytes } from "node:crypto"; +import * as net from "node:net"; +import { PassThrough } from "node:stream"; +import { + type KubeConfig, + KubeConfig as KubeConfigClass, + PortForward, +} from "@kubernetes/client-node"; +import { + bootstrapRepo, + daemonBash, + probeDaemonHealth, + proxyDaemonRequest, + readDaemonBootId, + waitForDaemonReady, +} from "../../daemon-client"; +import { + Inflight, + applyPreviewPattern, + hashSandboxId, + startDevServer, + stopDevServer, + withSandboxLock, +} from "../shared"; +import type { RunnerStateStore, RunnerStateStoreOps } from "../state-store"; +import type { + EnsureOptions, + ExecInput, + ExecOutput, + ProxyRequestInit, + Sandbox, + SandboxId, + SandboxRunner, + Workload, +} from "../types"; +import { + createSandboxClaim, + deleteSandboxClaim, + getSandboxClaim, + patchSandboxClaimShutdown, + waitForSandboxReady, + type SandboxClaim, + type SandboxResource, +} from "./client"; +import { K8S_CONSTANTS } from "./constants"; + +const RUNNER_KIND = "kubernetes" as const; +const LOG_LABEL = "KubernetesSandboxRunner"; + +// Shared-namespace topology for MVP; tenancy enforced by unguessable claim +// names (sha256(userId:projectRef)). Per-org namespaces are deferred. +const DEFAULT_NAMESPACE = "agent-sandbox-system"; +const DEFAULT_TEMPLATE_NAME = "mesh-sandbox"; + +const DAEMON_CONTAINER_PORT = 9000; +const DEV_CONTAINER_PORT = 3000; +const DEFAULT_WORKDIR = "/app"; + +// 32 bytes matches Docker's generation so audit logs don't vary by runner. +const DAEMON_TOKEN_BYTES = 32; + +// Default idle-reap TTL: 15 min from each ensure() hit. Every code-initiated +// request flows through ensure() (or touches a record via getRecord, which +// bumps the TTL on the K8s side), so an active sandbox pushes this forward; +// abandoned sandboxes roll off at T+15m via the operator. +const DEFAULT_IDLE_TTL_MS = 15 * 60 * 1000; + +/** Handle prefix + 16-hex hash = 24 chars, well under K8s's 63-char label cap. */ +const HANDLE_PREFIX = "mesh-sb-"; +const HANDLE_HASH_LEN = 16; + +// Deterministic local-port range for port-forward listeners. Same +// (handle, containerPort) pair → same host port across mesh restarts, so +// `previewUrl` cached in the thread's vmMap stays valid when the mesh +// process recycles. Birthday-collision probability stays <1% up to ~140 +// concurrent forwarders. EADDRINUSE walks the range forward until bind. +const PORT_RANGE_START = 40000; +const PORT_RANGE_SIZE = 10000; +const PORT_WALK_LIMIT = 256; + +// Structural type for the WebSocket returned by PortForward.portForward — we +// only need close/on to manage lifecycle; a direct `isomorphic-ws` dep for +// one type isn't worth it. +interface ForwardWebSocket { + close: () => void; + on: (event: "close" | "error", handler: () => void) => void; +} + +interface PortForwarder { + server: net.Server; + localPort: number; +} + +interface K8sRecord { + id: SandboxId; + handle: string; + podName: string; + token: string; + workdir: string; + daemonUrl: string; + daemonForward: PortForwarder; + devForward: PortForwarder | null; + repoAttached: boolean; + workload: Workload | null; + /** + * Per-daemon-boot UUID. `/app` lives in the container's ephemeral layer + * (see sandbox-template.yaml — no volumeMount), so any OOMKill/crash + * restart wipes the repo while the state store still says + * repoAttached: true. When this differs from the running daemon's bootId, + * rehydrate resets repoAttached so the next ensure re-bootstraps. Null + * means the daemon predates the bootId field — fall back to stored flag. + */ + daemonBootId: string | null; +} + +interface PersistedK8sState { + podName: string; + token: string; + workdir: string; + repoAttached?: boolean; + workload?: Workload | null; + daemonBootId?: string | null; + [k: string]: unknown; +} + +export interface KubernetesRunnerOptions { + stateStore?: RunnerStateStore; + previewUrlPattern?: string; + /** Defaults to `new KubeConfig().loadFromDefault()`. Tests pass a stub. */ + kubeConfig?: KubeConfig; + /** Shared namespace for both SandboxTemplate and SandboxClaims. */ + namespace?: string; + /** SandboxTemplate all claims reference. */ + sandboxTemplateName?: string; + /** + * Deterministic DAEMON_TOKEN override — tests inject a fixed value so + * recorded fetch payloads are stable. Prod leaves this undefined. + */ + tokenGenerator?: () => string; + /** + * Idle-reap window (ms). Every `ensure()` hit pushes the claim's + * `spec.lifecycle.shutdownTime` to `now + idleTtlMs`; the operator + * deletes claim + pod when the wall clock passes that. + */ + idleTtlMs?: number; +} + +export class KubernetesSandboxRunner implements SandboxRunner { + readonly kind = RUNNER_KIND; + + private readonly records = new Map(); + private readonly inflight = new Inflight(); + private readonly stateStore: RunnerStateStore | null; + private readonly previewUrlPattern: string | null; + private readonly kubeConfig: KubeConfig; + private readonly portForward: PortForward; + private readonly namespace: string; + private readonly sandboxTemplateName: string; + private readonly tokenGenerator: () => string; + private readonly idleTtlMs: number; + + constructor(opts: KubernetesRunnerOptions = {}) { + this.stateStore = opts.stateStore ?? null; + this.previewUrlPattern = opts.previewUrlPattern ?? null; + this.kubeConfig = opts.kubeConfig ?? loadDefaultKubeConfig(); + this.portForward = new PortForward(this.kubeConfig); + this.namespace = opts.namespace ?? DEFAULT_NAMESPACE; + this.sandboxTemplateName = + opts.sandboxTemplateName ?? DEFAULT_TEMPLATE_NAME; + this.tokenGenerator = + opts.tokenGenerator ?? + (() => randomBytes(DAEMON_TOKEN_BYTES).toString("hex")); + this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS; + } + + // ---- SandboxRunner surface ------------------------------------------------ + + async ensure(id: SandboxId, opts: EnsureOptions = {}): Promise { + const handle = this.computeHandle(id); + return this.inflight.run(handle, () => + withSandboxLock(this.stateStore, id, RUNNER_KIND, (ops) => + this.ensureLocked(id, handle, opts, ops), + ), + ); + } + + async exec(handle: string, input: ExecInput): Promise { + const rec = await this.requireRecord(handle); + return daemonBash(rec.daemonUrl, rec.token, input); + } + + async delete(handle: string): Promise { + const rec = await this.getRecord(handle); + this.records.delete(handle); + if (rec) { + await stopDevServer(rec.daemonUrl, rec.token, LOG_LABEL); + this.closeForwarder(rec.daemonForward); + if (rec.devForward) this.closeForwarder(rec.devForward); + } + await deleteSandboxClaim(this.kubeConfig, this.namespace, handle); + if (this.stateStore) { + if (rec) await this.stateStore.delete(rec.id, RUNNER_KIND); + else await this.stateStore.deleteByHandle(RUNNER_KIND, handle); + } + } + + async alive(handle: string): Promise { + const claim = await getSandboxClaim( + this.kubeConfig, + this.namespace, + handle, + ).catch(() => undefined); + return claim ? isSandboxReady(claim) : false; + } + + async getPreviewUrl(handle: string): Promise { + const rec = await this.getRecord(handle); + if (!rec) return null; + if (this.previewUrlPattern) { + return applyPreviewPattern(this.previewUrlPattern, handle); + } + // rehydrate() already opens devForward in local mode; if it's still null, + // the opener failed earlier and we try again now. + if (!rec.devForward) { + rec.devForward = await this.openForwarder( + rec.podName, + DEV_CONTAINER_PORT, + rec.handle, + ).catch(() => null); + } + return rec.devForward + ? `http://127.0.0.1:${rec.devForward.localPort}/` + : null; + } + + async proxyDaemonRequest( + handle: string, + path: string, + init: ProxyRequestInit, + ): Promise { + const rec = await this.getRecord(handle); + if (!rec) { + return new Response(JSON.stringify({ error: "sandbox not found" }), { + status: 404, + headers: { "content-type": "application/json" }, + }); + } + return proxyDaemonRequest(rec.daemonUrl, rec.token, path, init); + } + + // ---- Ensure flow ---------------------------------------------------------- + + private async ensureLocked( + id: SandboxId, + handle: string, + opts: EnsureOptions, + ops: RunnerStateStoreOps | null, + ): Promise { + if (opts.image) { + console.warn( + `[${LOG_LABEL}] opts.image ignored (template ${this.sandboxTemplateName} pins image): got ${opts.image}`, + ); + } + if (opts.env && Object.keys(opts.env).length > 0) { + console.warn( + `[${LOG_LABEL}] opts.env ignored (template holds non-token env; DAEMON_TOKEN is injected per-claim): keys=${Object.keys(opts.env).join(",")}`, + ); + } + + // 1. State-store resume. + if (ops) { + const persisted = await ops.get(id, RUNNER_KIND); + if (persisted) { + const rec = await this.rehydrate(id, handle, persisted); + if (rec) + return this.finish( + rec, + opts, + ops, + /* persistNow */ false, + /* patchTtl */ true, + ); + await ops.delete(id, RUNNER_KIND); + } + } + // 2. Cluster-side adopt: state store empty but a claim with our + // deterministic name already exists. + const existing = await getSandboxClaim( + this.kubeConfig, + this.namespace, + handle, + ).catch(() => undefined); + if (existing) { + const adopted = await this.adopt(id, handle, existing).catch((err) => { + console.warn( + `[${LOG_LABEL}] adopt ${handle} failed, recreating: ${err instanceof Error ? err.message : String(err)}`, + ); + return null; + }); + if (adopted) + return this.finish( + adopted, + opts, + ops, + /* persistNow */ true, + /* patchTtl */ true, + ); + await deleteSandboxClaim(this.kubeConfig, this.namespace, handle).catch( + () => {}, + ); + } + // 3. Fresh provision. + const fresh = await this.provision(id, handle, opts); + return this.finish( + fresh, + opts, + ops, + /* persistNow */ true, + /* patchTtl */ false, + ); + } + + private async finish( + rec: K8sRecord, + opts: EnsureOptions, + ops: RunnerStateStoreOps | null, + persistNow: boolean, + patchTtl: boolean, + ): Promise { + this.records.set(rec.handle, rec); + let bootstrapped = false; + if (opts.repo && !rec.repoAttached) { + await bootstrapRepo(rec.daemonUrl, rec.token, rec.workdir, opts.repo); + rec.repoAttached = true; + bootstrapped = true; + } + if (persistNow || bootstrapped) await this.persist(ops, rec); + startDevServer( + rec.daemonUrl, + rec.token, + opts.workload ?? rec.workload, + LOG_LABEL, + ); + // Fresh provision set a shutdownTime in the claim spec already; resumes + // and adopts rely on this patch to stay alive. + if (patchTtl) { + await patchSandboxClaimShutdown( + this.kubeConfig, + this.namespace, + rec.handle, + this.computeShutdownTime(), + ).catch((err) => + console.warn( + `[${LOG_LABEL}] TTL refresh failed for ${rec.handle}: ${err instanceof Error ? err.message : String(err)}`, + ), + ); + } + return this.toSandbox(rec); + } + + private async provision( + id: SandboxId, + handle: string, + opts: EnsureOptions, + ): Promise { + const token = this.tokenGenerator(); + const claim: SandboxClaim = { + apiVersion: `${K8S_CONSTANTS.CLAIM_API_GROUP}/${K8S_CONSTANTS.CLAIM_API_VERSION}`, + kind: "SandboxClaim", + metadata: { + name: handle, + namespace: this.namespace, + labels: { + "app.kubernetes.io/name": "mesh-sandbox", + "app.kubernetes.io/managed-by": "mesh", + }, + }, + spec: { + sandboxTemplateRef: { name: this.sandboxTemplateName }, + // Per-claim token. `valueFrom.secretKeyRef` isn't supported; RBAC on + // the namespace is the secrecy boundary. Warm-pool off because the + // operator rejects custom env on warm-pooled claims. + env: [{ name: "DAEMON_TOKEN", value: token }], + warmpool: "none", + lifecycle: { + shutdownPolicy: "Delete", + shutdownTime: this.computeShutdownTime(), + }, + }, + }; + + await createSandboxClaim(this.kubeConfig, this.namespace, claim); + const { podName } = await waitForSandboxReady( + this.kubeConfig, + this.namespace, + handle, + ); + + const daemonForward = await this.openForwarder( + podName, + DAEMON_CONTAINER_PORT, + handle, + ); + const daemonUrl = `http://127.0.0.1:${daemonForward.localPort}`; + try { + await waitForDaemonReady(daemonUrl); + } catch (err) { + this.closeForwarder(daemonForward); + await deleteSandboxClaim(this.kubeConfig, this.namespace, handle).catch( + () => {}, + ); + throw err; + } + + // Local-mode (Stage 1): eagerly open the dev port-forward so VM_START + // returns a preview URL that actually resolves, matching Docker. + const devForward = this.previewUrlPattern + ? null + : await this.openForwarder(podName, DEV_CONTAINER_PORT, handle).catch( + (err) => { + console.warn( + `[${LOG_LABEL}] dev port-forward for ${handle} failed: ${err instanceof Error ? err.message : String(err)}`, + ); + return null; + }, + ); + + return { + id, + handle, + podName, + token, + workdir: DEFAULT_WORKDIR, + daemonUrl, + daemonForward, + devForward, + repoAttached: false, + workload: opts.workload ?? null, + daemonBootId: await readDaemonBootId(daemonUrl), + }; + } + + /** + * Reconstruct a record from persisted state. After this returns, the record + * is ready for any of the six methods — daemon + dev port-forwards are + * open (local mode). Returns null on any mismatch; caller purges and falls + * through to adopt/provision. + */ + private async rehydrate( + id: SandboxId, + handle: string, + persisted: { handle: string; state: Record }, + ): Promise { + const state = persisted.state as Partial; + if (!state.podName || !state.token) return null; + + const claim = await getSandboxClaim( + this.kubeConfig, + this.namespace, + handle, + ).catch(() => undefined); + if (!claim || !isSandboxReady(claim)) return null; + + // Pod name may have changed (operator recreated the pod). Trust the claim + // annotation over the persisted value. + const currentPodName = readPodName(claim) ?? state.podName; + + const daemonForward = await this.openForwarder( + currentPodName, + DAEMON_CONTAINER_PORT, + handle, + ).catch(() => null); + if (!daemonForward) return null; + const daemonUrl = `http://127.0.0.1:${daemonForward.localPort}`; + + if (!(await probeDaemonHealth(daemonUrl))) { + this.closeForwarder(daemonForward); + return null; + } + + // Container restarted: pod is up but /app was wiped with its ephemeral + // layer. Force re-bootstrap by resetting repoAttached. If the daemon + // predates the bootId field, fall through to the persisted value. + const currentBootId = await readDaemonBootId(daemonUrl); + const rebooted = + currentBootId !== null && + state.daemonBootId != null && + currentBootId !== state.daemonBootId; + if (rebooted) { + console.warn( + `[${LOG_LABEL}] ${handle} pod rebooted (bootId ${state.daemonBootId} → ${currentBootId}); re-bootstrapping repo`, + ); + } + + // Local-mode: warm the dev forwarder so `previewUrl` cached in vmMap + // resolves immediately after mesh restart. Stage 3 skips this. + const devForward = this.previewUrlPattern + ? null + : await this.openForwarder( + currentPodName, + DEV_CONTAINER_PORT, + handle, + ).catch((err) => { + console.warn( + `[${LOG_LABEL}] dev port-forward for ${handle} failed: ${err instanceof Error ? err.message : String(err)}`, + ); + return null; + }); + + return { + id, + handle, + podName: currentPodName, + token: state.token, + workdir: state.workdir ?? DEFAULT_WORKDIR, + daemonUrl, + daemonForward, + devForward, + repoAttached: rebooted ? false : (state.repoAttached ?? false), + workload: state.workload ?? null, + daemonBootId: currentBootId, + }; + } + + private async adopt( + id: SandboxId, + handle: string, + claim: SandboxResource, + ): Promise { + if (!isSandboxReady(claim)) return null; + const podName = readPodName(claim); + if (!podName) return null; + const token = readClaimDaemonToken(claim); + if (!token) return null; + + const daemonForward = await this.openForwarder( + podName, + DAEMON_CONTAINER_PORT, + handle, + ); + const daemonUrl = `http://127.0.0.1:${daemonForward.localPort}`; + if (!(await probeDaemonHealth(daemonUrl))) { + this.closeForwarder(daemonForward); + return null; + } + + const devForward = this.previewUrlPattern + ? null + : await this.openForwarder(podName, DEV_CONTAINER_PORT, handle).catch( + () => null, + ); + + return { + id, + handle, + podName, + token, + workdir: DEFAULT_WORKDIR, + daemonUrl, + daemonForward, + devForward, + // Adopted: repo state unknown → next ensure re-runs attach if needed. + repoAttached: false, + workload: null, + daemonBootId: await readDaemonBootId(daemonUrl), + }; + } + + // ---- Handle resolution (post-restart) ------------------------------------- + + private async getRecord(handle: string): Promise { + const cached = this.records.get(handle); + if (cached) return cached; + if (!this.stateStore) return null; + const persisted = await this.stateStore.getByHandle(RUNNER_KIND, handle); + if (!persisted) return null; + const rec = await this.rehydrate(persisted.id, handle, persisted); + if (rec) this.records.set(handle, rec); + return rec; + } + + private async requireRecord(handle: string): Promise { + const rec = await this.getRecord(handle); + if (!rec) throw new Error(`unknown sandbox handle ${handle}`); + return rec; + } + + // ---- Identity + preview URL ---------------------------------------------- + + private computeHandle(id: SandboxId): string { + return `${HANDLE_PREFIX}${hashSandboxId(id, HANDLE_HASH_LEN)}`; + } + + private composePreviewUrl(rec: K8sRecord): string | null { + if (this.previewUrlPattern) { + return applyPreviewPattern(this.previewUrlPattern, rec.handle); + } + return rec.devForward + ? `http://127.0.0.1:${rec.devForward.localPort}/` + : null; + } + + private toSandbox(rec: K8sRecord): Sandbox { + return { + handle: rec.handle, + workdir: rec.workdir, + previewUrl: this.composePreviewUrl(rec), + }; + } + + // ---- Persistence ---------------------------------------------------------- + + private async persist( + ops: RunnerStateStoreOps | null, + rec: K8sRecord, + ): Promise { + if (!ops) return; + const state: PersistedK8sState = { + podName: rec.podName, + token: rec.token, + workdir: rec.workdir, + repoAttached: rec.repoAttached, + workload: rec.workload, + daemonBootId: rec.daemonBootId, + }; + await ops.put(rec.id, RUNNER_KIND, { handle: rec.handle, state }); + } + + // ---- TTL helpers ---------------------------------------------------------- + + private computeShutdownTime(): string { + return new Date(Date.now() + this.idleTtlMs).toISOString(); + } + + // ---- Port-forwarding ------------------------------------------------------ + + /** + * Opens a 127.0.0.1 TCP listener whose connections tunnel to + * `podName:containerPort` via the apiserver. Each TCP connection spawns a + * fresh WebSocket — matches `kubectl port-forward`'s semantics. Lifecycle + * is mutual: client socket close → close the k8s WS; WS close → destroy + * the client socket. + */ + private openForwarder( + podName: string, + containerPort: number, + // `handle` is passed separately so the deterministic port survives pod + // recreation (operator-driven): vmMap's cached previewUrl stays valid. + handle: string = podName, + ): Promise { + const startPort = deterministicLocalPort(handle, containerPort); + return new Promise((resolve, reject) => { + const tryBind = (port: number, attempt: number) => { + const server = net.createServer((socket) => + this.handleForwardedConnection(socket, podName, containerPort), + ); + server.once("error", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE" && attempt < PORT_WALK_LIMIT) { + const next = + PORT_RANGE_START + + ((port - PORT_RANGE_START + 1) % PORT_RANGE_SIZE); + tryBind(next, attempt + 1); + return; + } + reject(err); + }); + server.listen(port, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("port-forward listener failed to bind")); + return; + } + resolve({ server, localPort: address.port }); + }); + }; + tryBind(startPort, 0); + }); + } + + private handleForwardedConnection( + socket: net.Socket, + podName: string, + containerPort: number, + ): void { + // Inbound bytes pipe through a PassThrough rather than the socket + // directly: `portForward` attaches its 'data' listener only after the + // WebSocket opens (async); on Bun, bytes arriving in that window are + // dropped. Piping synchronously into a PassThrough buffers those bytes + // until the library drains it. + const inbound = new PassThrough(); + let ws: ForwardWebSocket | null = null; + let closed = false; + + const cleanup = () => { + if (closed) return; + closed = true; + inbound.destroy(); + if (ws) { + try { + ws.close(); + } catch {} + } + if (!socket.destroyed) socket.destroy(); + }; + + socket.pipe(inbound); + socket.on("error", cleanup); + socket.on("close", cleanup); + + this.portForward + .portForward( + this.namespace, + podName, + [containerPort], + socket, + null, + inbound, + ) + .then((res) => { + // retryCount=0 (default) → raw WebSocket; retryCount>0 → factory fn. + const opened = typeof res === "function" ? res() : res; + if (!opened) { + cleanup(); + return; + } + ws = opened as ForwardWebSocket; + ws.on("close", cleanup); + ws.on("error", cleanup); + if (closed) { + try { + ws.close(); + } catch {} + } + }) + .catch((err: unknown) => { + console.warn( + `[${LOG_LABEL}] port-forward to ${podName}:${containerPort} failed: ${err instanceof Error ? err.message : String(err)}`, + ); + cleanup(); + }); + } + + private closeForwarder(forwarder: PortForwarder): void { + forwarder.server.close((err) => { + if (err) { + console.warn( + `[${LOG_LABEL}] port-forward close on :${forwarder.localPort} errored: ${err instanceof Error ? err.message : String(err)}`, + ); + } + }); + } +} + +// ---- Helpers ---------------------------------------------------------------- + +function loadDefaultKubeConfig(): KubeConfig { + const kc = new KubeConfigClass(); + kc.loadFromDefault(); + return kc; +} + +function isSandboxReady(resource: SandboxResource): boolean { + return Boolean( + resource.status?.conditions?.some( + (c) => c.type === "Ready" && c.status === "True", + ), + ); +} + +function readClaimDaemonToken(claim: SandboxResource): string | null { + const env = claim.spec?.env; + if (!env) return null; + for (const entry of env) { + if (entry.name === "DAEMON_TOKEN" && entry.value) return entry.value; + } + return null; +} + +function readPodName(resource: SandboxResource): string | null { + return ( + resource.metadata?.annotations?.[K8S_CONSTANTS.POD_NAME_ANNOTATION] ?? + resource.metadata?.name ?? + null + ); +} + +function deterministicLocalPort(handle: string, containerPort: number): number { + const hash = createHash("sha256") + .update(`${handle}:${containerPort}`) + .digest(); + return PORT_RANGE_START + (hash.readUInt32BE(0) % PORT_RANGE_SIZE); +} diff --git a/packages/mesh-plugin-user-sandbox/server/runner/types.ts b/packages/mesh-plugin-user-sandbox/server/runner/types.ts index 7e6463fd74..7f191d0b7a 100644 --- a/packages/mesh-plugin-user-sandbox/server/runner/types.ts +++ b/packages/mesh-plugin-user-sandbox/server/runner/types.ts @@ -75,7 +75,7 @@ export interface ProxyRequestInit { * Persisted on `vmMap` and `sandbox_runner_state.runner_kind`. When widening, * keep `VmMapEntry.runnerKind` in sync. */ -export type RunnerKind = "docker" | "freestyle"; +export type RunnerKind = "docker" | "freestyle" | "kubernetes"; export interface SandboxRunner { readonly kind: RunnerKind; diff --git a/packages/mesh-sdk/src/types/virtual-mcp.ts b/packages/mesh-sdk/src/types/virtual-mcp.ts index b2e44c95cf..bf0133220a 100644 --- a/packages/mesh-sdk/src/types/virtual-mcp.ts +++ b/packages/mesh-sdk/src/types/virtual-mcp.ts @@ -137,6 +137,8 @@ export type GithubRepo = z.infer; * `runnerKind` lets the UI construct daemon URLs correctly: * - docker: daemon is reached via the mesh proxy at `/api/sandbox//_daemon/*` * - freestyle: daemon lives at `${previewUrl}/_decopilot_vm/*` on the VM domain + * - kubernetes: daemon is reached via the mesh proxy (same transport as docker); + * preview URL is the per-claim HTTPRoute host (in-cluster) or a local port-forward (kind dev). * * `previewUrl` is nullable: blank / tool sandboxes (no `workload`, no dev * server) have nothing to render. UI code MUST check before constructing @@ -152,7 +154,7 @@ export const VmMapEntrySchema = z.object({ .describe( "URL where the VM's iframe-proxied UI is served, or null when the sandbox has no dev server (blank / tool sandboxes).", ), - runnerKind: z.enum(["docker", "freestyle"]).optional(), + runnerKind: z.enum(["docker", "freestyle", "kubernetes"]).optional(), createdAt: z .number() .optional() From 5242f9013ed136aee50527a21169407be5491ed1 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Fri, 24 Apr 2026 12:49:15 -0300 Subject: [PATCH 02/16] chore: update dependencies and versions in bun.lock - Bumped version of decocms to 2.274.0. - Updated various dependencies including @ai-sdk/anthropic, @ai-sdk/gateway, @ai-sdk/google, @ai-sdk/openai, and @ai-sdk/provider-utils to their latest versions. - Added new entries for @anthropic-ai/claude-agent-sdk and its platform-specific variants. --- bun.lock | 772 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 394 insertions(+), 378 deletions(-) diff --git a/bun.lock b/bun.lock index 1f4d7ab145..e52cb68160 100644 --- a/bun.lock +++ b/bun.lock @@ -53,7 +53,7 @@ }, "apps/mesh": { "name": "decocms", - "version": "2.272.8", + "version": "2.274.0", "bin": { "deco": "./dist/server/cli.js", }, @@ -186,10 +186,6 @@ "@freestyle-sh/with-deno": "^0.0.4", "@freestyle-sh/with-nodejs": "^0.2.9", "freestyle-sandboxes": "^0.1.46", - "@freestyle-sh/with-bun": "^0.2.12", - "@freestyle-sh/with-deno": "^0.0.4", - "@freestyle-sh/with-nodejs": "^0.2.9", - "freestyle-sandboxes": "^0.1.46", }, }, "packages/bindings": { @@ -432,23 +428,39 @@ "fast-xml-parser": "5.4.2", }, "packages": { - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], - "@ai-sdk/google": ["@ai-sdk/google@3.0.60", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ye/hG0LeO24VmjLbfgkFZV8V8k/l4nVBODutpJQkFPyUiGOCbFtFUTgxSeC7+njrk5+HhgyHrzJay4zmhwMH+w=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.64", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CbR82EgGPNrj/6q0HtclwuCqe0/pDShyv3nWDP/A9DroujzWXnLMlUJVrgPOsg4b40zQCwwVs2XSKCxvt/4QaA=="], - "@ai-sdk/openai": ["@ai-sdk/openai@3.0.50", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7M7bklrS+gckzPdpQpC3iG5aN5aQPRJdAJQ5jt7sEgYCqDgUuef9x4Nd570+ghIfKTZvV6tSqeeTuD6De/bZig=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], - "@ai-sdk/react": ["@ai-sdk/react@3.0.118", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.19", "ai": "6.0.116", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-fBAix8Jftxse6/2YJnOFkwW1/O6EQK4DK68M9DlFmZGAzBmsaHXEPVS77sVIlkaOWCy11bE7434NAVXRY+3OsQ=="], + "@ai-sdk/react": ["@ai-sdk/react@3.0.170", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.23", "ai": "6.0.168", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-YUDn+mK0c8iUz14rCBf1A0zg6SV5b5aSVUz+azF1bdBd1SFXVI19dKYR+PQSpZY+0+z+zs252AAsacUqiO98Kw=="], "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], - "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.80", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-9vMzf38ZeV8b70RPcaC/ZrJZKL6G56KTS8zXoBbBBRkPDLCgrGLC8sP2AfrR62eliSkw5u+sOcP9kKX+t54CHg=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.119", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.119", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.119", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.119", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.119", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.119", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.119", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.119", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.119" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-6AvthpsaOTlkn514brSGOcCSLHDXODnU+ExN1O3CJCjxr5RBcmzR057C9EIM0G7IchnXsRfMZgRO1QKsjTXdbA=="], + + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.119", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kxnG37SZqUata2Jcp/YQ0n9Y7o/sinE/8LdG4ltM1gePh+z+0Mfa4vBUUTEBMBFth9PTovKoesIuVuyFpvO/Cw=="], + + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.119", "", { "os": "darwin", "cpu": "x64" }, "sha512-9Aj8g3ELsmZuOFg17TCkikeg/Wt2ucVT8hOOPQUatzLd7BKhydrHLA0RP42nBpWECO1B/n/mPdQ4iS/LS3s2Fg=="], + + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.119", "", { "os": "linux", "cpu": "arm64" }, "sha512-v3o464XkiYehp/OKidQQirxdVb+aGSvdJvHF2zH9p33W8M/NC21zwwh4dhwDnKsyrtBIgkt2CcMwzIl30r0OtA=="], + + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.119", "", { "os": "linux", "cpu": "arm64" }, "sha512-IPGWgtz+gGnD7fxKAvSf913EUT/lYBTBE8EZ7lh3+x5ZP2859LWLmrCm053Lf3nMWo/CWikZsVPwkDVwpz6tIQ=="], + + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.119", "", { "os": "linux", "cpu": "x64" }, "sha512-9ePt4ZN+hsqDw4AgS4KtcWIGKfL9Oq28kwkrTER/QAcSrVKxiLonp81cCLzg7Ok/IUJu4Cfd71GZbFv/WE54zw=="], + + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.119", "", { "os": "linux", "cpu": "x64" }, "sha512-QYxFNAe4FFridPkKhGlNcNBJ0TaIygWYyvfI9g4kX0i+RVbresUWuZVkWY06ioJ0fXoixFJ+HNQBMB7dLrIp8Q=="], + + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.119", "", { "os": "win32", "cpu": "arm64" }, "sha512-p/TjcKQvkCYtXGPlR+mdyNwqCmvRcQL34Wtq0yUZ+iqmI/eyCe59IJ3AZrE0EZoqmiAevEYzatPIt9sncC9uxw=="], + + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.119", "", { "os": "win32", "cpu": "x64" }, "sha512-k98Ju0wtktm6FhqTE/cXlVr6K4kGqBolVjEGzeKkW6ZILc7124euwNapAvkQCwMAavAxS/ZnO3jdKMtHtwTVTA=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.79.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-ietmtM6glcnnrWq26H+BZm8J07iay9Cob6hRzDTr/A9QWF1m2T//TQhFO4MTKcZht2/7LS8bG9wUYEhcizKRnA=="], @@ -484,73 +496,73 @@ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1013.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.22", "@aws-sdk/credential-provider-node": "^3.972.23", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.2", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.22", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.23", "@aws-sdk/region-config-resolver": "^3.972.8", "@aws-sdk/signature-v4-multi-region": "^3.996.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.9", "@smithy/config-resolver": "^4.4.11", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.26", "@smithy/middleware-retry": "^4.4.43", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.42", "@smithy/util-defaults-mode-node": "^4.2.45", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-vFdyRyRatF+xP9Fi+4alZkmzZadqOAM34Pm6SUZsYtumNrWkgMc/pFWITnsq6eltM8qcV/vcinQ1ZBXWm/PlKg=="], + "@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1036.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.5", "@aws-sdk/credential-provider-node": "^3.972.36", "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", "@aws-sdk/middleware-expect-continue": "^3.972.10", "@aws-sdk/middleware-flexible-checksums": "^3.974.13", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-location-constraint": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-sdk-s3": "^3.972.34", "@aws-sdk/middleware-ssec": "^3.972.10", "@aws-sdk/middleware-user-agent": "^3.972.35", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/signature-v4-multi-region": "^3.996.22", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.21", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-blob-browser": "^4.2.15", "@smithy/hash-node": "^4.2.14", "@smithy/hash-stream-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/md5-js": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-retry": "^4.5.5", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.49", "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.4", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.16", "tslib": "^2.6.2" } }, "sha512-QGjLHw1xklwWX+MWt/7X66lMxjNQLOb0tjcwAU3PaBrYZ51kphDlfvc2sInNEsIU03+I158Y4WSMhl8l71SAsw=="], - "@aws-sdk/core": ["@aws-sdk/core@3.973.22", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.14", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-lY6g5L95jBNgOUitUhfV2N/W+i08jHEl3xuLODYSQH5Sf50V+LkVYBSyZRLtv2RyuXZXiV7yQ+acpswK1tlrOA=="], + "@aws-sdk/core": ["@aws-sdk/core@3.974.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.19", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.4", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-lMPlYlYfQdNZhlkJgnkmESwrY+hNh3PljmZ+37oAqLNdJ6rnILAwFSyc6B3bJeDOtMORNnMQIej0aTRuOlDyhQ=="], - "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.5", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg=="], + "@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.7", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-vI0QN96DFx3g9AunfOWF3CS4cMkqFiR/WM/FyP9QHr5rZ2dKPkYwP3tCgAOvGuu9CXI7dC1vU2FVUuZ+tfpNvQ=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-X/yGB73LmDW/6MdDJGCDzZBUXnM3ys4vs9l+5ZTJmiEswDdP1OjeoAFlFjVGS9o4KB2wZWQ9KOfdVNSSK6Ep3w=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-aS/81smalpe7XDnuQfOq4LIPuaV2PRKU2aMTrHcqO5BD4HwO5kESOHNcec2AYfBtLtIDqgF6RXisgBnfK/jt0w=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.33", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.6.1", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" } }, "sha512-c0ZF+lwoWVvX5iCaGKL5T/4DnIw88CGqxA0BcBs3U86mIp5EZYPVg+KSPkMXOyokmADvNewiMUfSG2uFwjRp0g=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/credential-provider-env": "^3.972.20", "@aws-sdk/credential-provider-http": "^3.972.22", "@aws-sdk/credential-provider-login": "^3.972.22", "@aws-sdk/credential-provider-process": "^3.972.20", "@aws-sdk/credential-provider-sso": "^3.972.22", "@aws-sdk/credential-provider-web-identity": "^3.972.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-rpF8fBT0LllMDp78s62aL2A/8MaccjyJ0ORzqu+ZADeECLSrrCWIeeXsuRam+pxiAMkI1uIyDZJmgLGdadkPXw=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.35", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/credential-provider-env": "^3.972.31", "@aws-sdk/credential-provider-http": "^3.972.33", "@aws-sdk/credential-provider-login": "^3.972.35", "@aws-sdk/credential-provider-process": "^3.972.31", "@aws-sdk/credential-provider-sso": "^3.972.35", "@aws-sdk/credential-provider-web-identity": "^3.972.35", "@aws-sdk/nested-clients": "^3.997.3", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-jsU4u/cRkKFLKQS0k918FQ27fzXLG5ENiLWQMYE6581zLeI2hWh04ptlrvZMB3wJT/5d+vSzJk74X1CMFr4y8Q=="], - "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-u33CO9zeNznlVSg9tWTCRYxaGkqr1ufU6qeClpmzAabXZa8RZxQoVXxL5T53oZJFzQYj+FImORCSsi7H7B77gQ=="], + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.35", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/nested-clients": "^3.997.3", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-5oa3j0cA50jPqgNhZ9XdJVopuzUf1klRb28/2MfLYWWiPi9DRVvbrBWT+DidbHTT36520VuXZJahQwR+YgSjrg=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.23", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.20", "@aws-sdk/credential-provider-http": "^3.972.22", "@aws-sdk/credential-provider-ini": "^3.972.22", "@aws-sdk/credential-provider-process": "^3.972.20", "@aws-sdk/credential-provider-sso": "^3.972.22", "@aws-sdk/credential-provider-web-identity": "^3.972.22", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-U8tyLbLOZItuVWTH0ay9gWo4xMqZwqQbg1oMzdU4FQSkTpqXemm4X0uoKBR6llqAStgBp30ziKFJHTA43l4qMw=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.36", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.31", "@aws-sdk/credential-provider-http": "^3.972.33", "@aws-sdk/credential-provider-ini": "^3.972.35", "@aws-sdk/credential-provider-process": "^3.972.31", "@aws-sdk/credential-provider-sso": "^3.972.35", "@aws-sdk/credential-provider-web-identity": "^3.972.35", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-4nT2T8Z7vH8KE9EdjEsuIlHpZSlcaK2PrKbQBjuUGU46BCCzF3WvP0u0Uiosni3Ykmmn4rWLVawoOCLotUtCbg=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.20", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-QRfk7GbA4/HDRjhP3QYR6QBr/QKreVoOzvvlRHnOuGgYJkeoPgPY3LAI1kK1ZMgZ4hH9KiGp757/ntol+INAig=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.31", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-eKeT4MXumpBJsrDLCYcSzIkFPVTFn/es7It2oogp2OhU/ic7P/+xzFpQx9ZhwtXS57Mc5S42BPWi7lHmvs/nYg=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/token-providers": "3.1013.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-4vqlSaUbBj4aNPVKfB6yXuIQ2Z2mvLfIGba2OzzF6zUkN437/PGWsxBU2F8QPSFHti6seckvyCXidU3H+R8NvQ=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.35", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/nested-clients": "^3.997.3", "@aws-sdk/token-providers": "3.1036.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-bCuBdfnj0KGDMdLp6utMTLiJcFN2ek9EgZinxQZZSc3FxjJ/HSqeqab2cjbnoNfy8RM6suDCsRkmVY1izp9I+A=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/wN1CYg2rVLhW8/jLxMWacQrkpaynnL+4j/Z+e6X1PfoE6NiC0BeOw3i0JmtZrKun85wNV5GmspvuWJihfeeUw=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.35", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/nested-clients": "^3.997.3", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-swW6Bwvl8lanyEMtZOWE/oR6yqcRQH4HTQZUVsnDVgoXvRjRywpYpLv2BWwjUFyjPrqsdX6FeTkf4tMSe/qFTQ=="], - "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA=="], - "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ=="], + "@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ=="], - "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.2", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.22", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4soN/N4R6ptdnHw7hXPVDZMIIL+vhN8rwtLdDyS0uD7ExhadtJzolTBIM5eKSkbw5uBEbIwtJc8HCG2NM6tN/g=="], + "@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.13", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.974.5", "@aws-sdk/crc64-nvme": "^3.972.7", "@aws-sdk/types": "^3.973.8", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-b6QUe2hQX9XsnCzp6mtzVaERhganDKeb8lmGL6pVhr7rRVH9S9keDFW7uKytuuqmcY5943FixoGqn/QL+sbUBA=="], - "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="], + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg=="], - "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw=="], + "@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ=="], - "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="], + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ=="], - "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA=="], + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.11", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ=="], - "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.22", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-dkUcRxF4rVpPbyHpxjCApGK6b7JpnSeo7tDoNakpRKmiLMCqgy4tlGBgeEYJnZgLrA4xc5jVKuXgvgqKqU18Kw=="], + "@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.34", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-/UL96JKjsjdodcRRMKl99tLQvK6Oi9ptLC9iU1yiTF/ruaDX0mtBBtnLNZDxIZRJOCVOtB49ed1YaTadqygk8Q=="], - "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw=="], + "@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw=="], - "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-HQu8QoqGZZTvg0Spl9H39QTsSMFwgu+8yz/QGKndXFLk9FZMiCiIgBCVlTVKMDvVbgqIzD9ig+/HmXsIL2Rb+g=="], + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.35", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-retry": "^4.3.4", "tslib": "^2.6.2" } }, "sha512-hOFWNOjVmOocpRlrU04nYxjMOeoe0Obu5AXEuhB8zblMCPl3cG1hdluQCZERRKFyhMQjwZnDbhSHjoMUjetFGw=="], - "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.12", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.22", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.23", "@aws-sdk/region-config-resolver": "^3.972.8", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.9", "@smithy/config-resolver": "^4.4.11", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.26", "@smithy/middleware-retry": "^4.4.43", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.42", "@smithy/util-defaults-mode-node": "^4.2.45", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KLdQGJPSm98uLINolQ0Tol8OAbk7g0Y7zplHJ1K83vbMIH13aoCvR6Tho66xueW4l4aZlEgVGLWBnD8ifUMsGQ=="], + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.3", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.5", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.35", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/signature-v4-multi-region": "^3.996.22", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.21", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-retry": "^4.5.5", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.49", "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.4", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-SivE6GP228IVgfsrr2c/vqTg95X0Qj39Yw4uIrcddpkUzIltNMoNOR62leHOLhODfjv9K8X2mPTwS69A5kT0nQ=="], - "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.11", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw=="], + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.13", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A=="], - "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.1013.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "^3.996.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", "@smithy/middleware-endpoint": "^4.4.26", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ndMG/QJ3W9ViBKiMUI/PQv9cYVXuLFtP0PC6Z1Dqn3GTlBufSO8vkGuU2X90aAKcoQ6U/GsUeDrjYVio5d2GjQ=="], + "@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.1036.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "^3.996.22", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-format-url": "^3.972.10", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-5LEXxcZrrCrPvd7/zIyadEIBsMeXyT6sg9tn3OkkwZ9Rl6Wfq7gxOI82Ei30cj0QXvzJwjcizpskmPh8M5QNtQ=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.10", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.22", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-yJSbFTedh1McfqXa9wZzjchqQ2puq5PI/qRz5kUjg2UXS5mO4MBYBbeXaZ2rp/h+ZbkcYEdo4Qsiah9psyoxrA=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.22", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.34", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-/rXhMXteD+BqhFd0nYprAgcZ/KtU+963uftPqd3tiFcFfooHZINXUGtOmo2SQjRVauCTNqIEzkwuSETdZFqTTA=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1013.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.22", "@aws-sdk/nested-clients": "^3.996.12", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-IL1c54UvbuERrs9oLm5rvkzMciwhhpn1FL0SlC3XUMoLlFhdBsWJgQKK8O5fsQLxbFVqjbjFx9OBkrn44X9PHw=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1036.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.5", "@aws-sdk/nested-clients": "^3.997.3", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-aNSJ6jjDYayxN9ZA1JpycVScX93Lx03kKZ1EXt3DGOTahcWVLJj3oLAlop0xKP+vP2Ga2t49p1tEaMkTbCCaZA=="], - "@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="], + "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], "@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="], - "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="], + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-endpoints": "^3.4.2", "tslib": "^2.6.2" } }, "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g=="], - "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A=="], + "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ=="], "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="], - "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="], + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g=="], - "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.9", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-jeFqqp8KD/P5O+qeKxyGeu7WEVIZFNprnkaDjGmBOjwxYwafCBhpxTgV1TlW6L8e76Vh/siNylNmN/OmSIFBUQ=="], + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.21", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.35", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-Av4UHTcAWgdvbN0IP9pbtf4Qa1+6LtJqQdZWj5pLn5J67w0pnJJAZZ+7JPPcj2KN3378zD2JDM9DwJKEyvyMTQ=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.14", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.6", "tslib": "^2.6.2" } }, "sha512-G/Yd8Bnnyh8QrqLf8jWJbixEnScUFW24e/wOBGYdw1Cl4r80KX/DvHyM2GVZ2vTp7J4gTEr8IXJlTadA8+UfuQ=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.19", "", { "dependencies": { "@smithy/types": "^4.14.1", "fast-xml-parser": "5.7.1", "tslib": "^2.6.2" } }, "sha512-Cw8IOMdBUEIl8ZlhRC3Dc/E64D5B5/8JhV6vhPLiPfJwcRC84S6F8aBOIi/N4vR9ZyA4I5Cc0Ateb/9EHaJXeQ=="], "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], @@ -578,15 +590,15 @@ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], - "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], - "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], - "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], @@ -594,9 +606,11 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@better-auth/api-key": ["@better-auth/api-key@1.6.9", "", { "dependencies": { "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "better-auth": "^1.6.9" } }, "sha512-MPDNmvcCwDpix911kFYRn9XCebJjaCNuj16OA//difNCJoPRn2kD6KQ/3+B3rlSWl46x098SgN7Y3e8kU8nIwg=="], + "@better-auth/core": ["@better-auth/core@1.4.6-beta.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-yjiu7wva4a0HFiWNWoaKfazLXMOx0+2mpyIbB5A1ov1Ta/YXZAg7jeh9A9uyBGXI8Y2qBVMYExumXVp1m1Xz+w=="], - "@better-auth/passkey": ["@better-auth/passkey@1.5.5", "", { "dependencies": { "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.3", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "better-auth": "1.5.5", "better-call": "1.3.2", "nanostores": "^1.0.1" } }, "sha512-waGngvVophgoi/yqyU8fPZS04ZRMfjPBlxRlbV49nOgqFXcA9+914cGUedmaePXlTH6q6Z9K3dXUlg8H4g5tTQ=="], + "@better-auth/passkey": ["@better-auth/passkey@1.6.9", "", { "dependencies": { "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.3", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.9", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "better-auth": "^1.6.9", "better-call": "1.3.5", "nanostores": "^1.0.1" } }, "sha512-MFpi+2G/pG2wVcTuL/PcnWxP2ddFL4jmFByTCbgvr61tp7u96d5liBptxpTqfS5IuCi2o8bBRmjiQnDZAvxmHg=="], "@better-auth/sso": ["@better-auth/sso@1.4.1", "", { "dependencies": { "@better-fetch/fetch": "1.1.18", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "samlify": "^2.10.1", "zod": "^4.1.12" }, "peerDependencies": { "better-auth": "1.4.1" } }, "sha512-EG3P6uxieSFQvOR4Xxs210YrIrLGGlUDluvPxKx1qMgVxfbt3S8ziQ3wnLScTJKe3WjzsCX8JdN6hLhbO76YAA=="], @@ -624,41 +638,41 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw=="], - "@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="], + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.0", "", {}, "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA=="], "@capsizecss/unpack": ["@capsizecss/unpack@4.0.0", "", { "dependencies": { "fontkitten": "^1.0.0" } }, "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA=="], - "@captchafox/react": ["@captchafox/react@1.11.0", "", { "dependencies": { "@captchafox/types": "^1.4.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-/oF/PahBiNwk/ZVC0XQBl9hHwwRo7Eg8k6tz0U835jH15OJpIZvGHPN4xZ5cRTct+sXAdVcMqfuEP0bsg4yTBw=="], + "@captchafox/react": ["@captchafox/react@1.12.0", "", { "dependencies": { "@captchafox/types": "^1.4.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-ALJWVvSBL3H16KMrGg3exO6EufI32zY63LBB0UC0l7jW+Ynz832tG7wwg0PbDWNle0CiN35DAfxDzH/29sDp3g=="], - "@captchafox/types": ["@captchafox/types@1.4.0", "", {}, "sha512-4xnPMICLinsXghw6zWEF436lhMsBmLxui2QmU7xAEz/+572BRdvc518Sz5OoofbJ7GZG6QPz/wOtJoN8BfKyCg=="], + "@captchafox/types": ["@captchafox/types@1.5.0", "", {}, "sha512-uReDnNoAarRjhbxC0w4hEueKLCx8w22dv+XqzDjR4rUFo3fCj8Dh3A6D27OFAYlfSr/nwomDatAxz7ixwJ5y5w=="], - "@clickhouse/client": ["@clickhouse/client@1.18.2", "", { "dependencies": { "@clickhouse/client-common": "1.18.2" } }, "sha512-fuquQswRSHWM6D079ZeuGqkMOsqtcUPL06UdTnowmoeeYjVrqisfVmvnw8pc3OeKS4kVb91oygb/MfLDiMs0TQ=="], + "@clickhouse/client": ["@clickhouse/client@1.18.3", "", { "dependencies": { "@clickhouse/client-common": "1.18.3" } }, "sha512-340ngdYktL8PLUBK2QKSwe0o02tYfZSz1mSn1uXCEU8TxHvwh9pnQxElf9YHumDGj5gX/IdgxPsJTGMs82Hgug=="], - "@clickhouse/client-common": ["@clickhouse/client-common@1.18.2", "", {}, "sha512-J0SG6q9V31ydxonglpj9xhNRsUxCsF71iEZ784yldqMYwsHixj/9xHFDgBDX3DuMiDx/kPDfXnf+pimp08wIBA=="], + "@clickhouse/client-common": ["@clickhouse/client-common@1.18.3", "", {}, "sha512-3axzO3zvrsGT5PzDenxgWscltYCNRDbhaHWUgdsmcM9OnW/VnZn9EarOcZogr9P82Z0mQh+Jd2x+p2K4TFD2fA=="], "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="], - "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.15.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw=="], + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], - "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.28.0", "", { "dependencies": { "@cloudflare/unenv-preset": "2.15.0", "miniflare": "4.20260312.0", "unenv": "2.0.0-rc.24", "wrangler": "4.73.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-h2idr5fZ5GojyWZOZ506NHaDAVq3zpvcKgk8ZzDLlnHHvOwXZlFDPRf9Kkffv0fe+J6GPn7gVjJxgT0YRXAbew=="], + "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.33.2", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "4.20260424.0", "unenv": "2.0.0-rc.24", "wrangler": "4.85.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" } }, "sha512-XI6+XkDn8W6tlvtYUoS6C89Te7fwhyDLrhUBUbagPO1StJ6ofbR0vpqNNqNskUp9592xTRCDk5ukqcjz6xMo+g=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260312.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HUAtDWaqUduS6yasV6+NgsK7qBpP1qGU49ow/Wb117IHjYp+PZPUGReDYocpB4GOMRoQlvdd4L487iFxzdARpw=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260424.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-yFR1XaJbSDLg/qbwtrYaU2xwFXatIPKR5nrMQCN1q/m6+Qe/j6r+kCnFEvOJjMZOm9iCKsE6Qly5clgl4u32qw=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260312.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DOn7TPTHSxJYfi4m4NYga/j32wOTqvJf/pY4Txz5SDKWIZHSTXFyGz2K4B+thoPWLop/KZxGoyTv7db0mk/qyw=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260424.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LqWKcE7x/9KyC2iQvKPeb20hKST3dYXDZlYTvFymgR1DfLS0OFOCzVGTloVNd7WqvK4SkdzBYfxo7QMIAeBK0w=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260312.1", "", { "os": "linux", "cpu": "x64" }, "sha512-TdkIh3WzPXYHuvz7phAtFEEvAxvFd30tHrm4gsgpw0R0F5b8PtoM3hfL2uY7EcBBWVYUBtkY2ahDYFfufnXw/g=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260424.1", "", { "os": "linux", "cpu": "x64" }, "sha512-YlEBFbAYZHe/ylzl8WEYQEU/jr+0XMqXaST2oBk5oVjksdb1NGuJaggluCdZAzuJJ8UqdTmyhY5u/qrasbiFWA=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260312.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-kNauZhL569Iy94t844OMwa1zP6zKFiL3xiJ4tGLS+TFTEfZ3pZsRH6lWWOtkXkjTyCmBEOog0HSEKjIV4oAffw=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260424.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-qJ0X0m6cL8fWDUPDg8K4IxYZXNJI6XbeOihqjnqKbAClrjdPDn8VUSd+z2XiCQ5NylMtMrpa/skC9UfaR6mh8g=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260312.1", "", { "os": "win32", "cpu": "x64" }, "sha512-5dBrlSK+nMsZy5bYQpj8t9iiQNvCRlkm9GGvswJa9vVU/1BNO4BhJMlqOLWT24EmFyApZ+kaBiPJMV8847NDTg=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260424.1", "", { "os": "win32", "cpu": "x64" }, "sha512-tZ7Z9qmYNAP6z1/+8r/zKbk8F8DZmpmwNzMeN+zkde2Wnhfr3FBqOkJXT/5zmli8HPoWrIXxSiyqcNDMy8V2Zg=="], - "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260313.1", "", {}, "sha512-jMEeX3RKfOSVqqXRKr/ulgglcTloeMzSH3FdzIfqJHtvc12/ELKd5Ldsg8ZHahKX/4eRxYdw3kbzb8jLXbq/jQ=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260424.1", "", {}, "sha512-0DLJ9yEk1KKzPbqop80Gw/P1wkKKzawmipULiJWdBXIBCoMvE0OVWms3IrL/Q/G7tfmPop9yF4XlZ69k9JLYng=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], "@daveyplate/better-auth-tanstack": ["@daveyplate/better-auth-tanstack@1.3.6", "", { "peerDependencies": { "@tanstack/query-core": ">=5.65.0", "@tanstack/react-query": ">=5.65.0", "better-auth": ">=1.2.8", "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-GvIGdbjRMZCEfAffU7LeWpGpie4vSli8V9jmNFCQOziZZMEFbO4cd53HBFmAushC9oEYIyhSNZBgV2ADzW94Ww=="], - "@daveyplate/better-auth-ui": ["@daveyplate/better-auth-ui@3.3.15", "", { "dependencies": { "@better-fetch/fetch": "^1.1.21", "@hcaptcha/react-hcaptcha": "^1.17.1", "@noble/hashes": "^2.0.1", "@react-email/components": "^1.0.1", "@wojtekmaj/react-recaptcha-v3": "^0.1.4", "react-google-recaptcha": "^3.1.0", "react-qr-code": "^2.0.18", "ua-parser-js": "^2.0.7", "vaul": "^1.1.2" }, "peerDependencies": { "@better-auth/passkey": ">=1.4.6", "@captchafox/react": "^1.10.0", "@daveyplate/better-auth-tanstack": "^1.3.6", "@hookform/resolvers": ">=5.2.0", "@instantdb/react": ">=0.18.0", "@marsidev/react-turnstile": ">=1.1.0", "@radix-ui/react-avatar": ">=1.1.0", "@radix-ui/react-checkbox": ">=1.1.0", "@radix-ui/react-context": ">=1.1.0", "@radix-ui/react-dialog": ">=1.1.0", "@radix-ui/react-dropdown-menu": ">=2.1.0", "@radix-ui/react-label": ">=2.1.0", "@radix-ui/react-primitive": ">=2.0.0", "@radix-ui/react-select": ">=2.2.0", "@radix-ui/react-separator": ">=1.1.0", "@radix-ui/react-slot": ">=1.1.0", "@radix-ui/react-tabs": ">=1.1.0", "@radix-ui/react-tooltip": ">=1.2.0", "@radix-ui/react-use-callback-ref": ">=1.1.0", "@radix-ui/react-use-layout-effect": ">=1.1.0", "@tanstack/react-query": ">=5.66.0", "@triplit/client": ">=1.0.0", "@triplit/react": ">=1.0.0", "better-auth": "^1.4.6", "class-variance-authority": ">=0.7.0", "clsx": ">=2.1.0", "input-otp": ">=1.4.0", "lucide-react": ">=0.469.0", "react": ">=18.0.0", "react-dom": ">=18.0.0", "react-hook-form": ">=7.55.0", "sonner": ">=1.7.0", "tailwind-merge": ">=2.6.0", "tailwindcss": ">=3.0.0", "zod": ">=3.0.0" } }, "sha512-vvsQ70EJha+WTBKjLbw4U/ycRjL0IgKHZ3RphZAONGnR/2BfdfVn8CEfrrsydmkYKMB8HGxtoMiU7/uu3qD72g=="], + "@daveyplate/better-auth-ui": ["@daveyplate/better-auth-ui@3.4.0", "", { "dependencies": { "@better-auth/api-key": "^1.5.6", "@better-fetch/fetch": "^1.1.21", "@hcaptcha/react-hcaptcha": "^2.0.2", "@noble/hashes": "^2.0.1", "@react-email/components": "^1.0.10", "@wojtekmaj/react-recaptcha-v3": "^0.1.4", "better-call": "2.0.2", "bowser": "^2.11.0", "react-google-recaptcha": "^3.1.0", "react-qr-code": "^2.0.18", "vaul": "^1.1.2" }, "peerDependencies": { "@better-auth/passkey": ">=1.4.6", "@captchafox/react": "^1.10.0", "@daveyplate/better-auth-tanstack": "^1.3.6", "@hookform/resolvers": ">=5.2.0", "@instantdb/react": ">=0.18.0", "@marsidev/react-turnstile": ">=1.1.0", "@radix-ui/react-avatar": ">=1.1.0", "@radix-ui/react-checkbox": ">=1.1.0", "@radix-ui/react-context": ">=1.1.0", "@radix-ui/react-dialog": ">=1.1.0", "@radix-ui/react-dropdown-menu": ">=2.1.0", "@radix-ui/react-label": ">=2.1.0", "@radix-ui/react-primitive": ">=2.0.0", "@radix-ui/react-select": ">=2.2.0", "@radix-ui/react-separator": ">=1.1.0", "@radix-ui/react-slot": ">=1.1.0", "@radix-ui/react-tabs": ">=1.1.0", "@radix-ui/react-tooltip": ">=1.2.0", "@radix-ui/react-use-callback-ref": ">=1.1.0", "@radix-ui/react-use-layout-effect": ">=1.1.0", "@tanstack/react-query": ">=5.66.0", "@triplit/client": ">=1.0.0", "@triplit/react": ">=1.0.0", "better-auth": "^1.4.6", "class-variance-authority": ">=0.7.0", "clsx": ">=2.1.0", "input-otp": ">=1.4.0", "lucide-react": ">=0.469.0", "react": ">=18.0.0", "react-dom": ">=18.0.0", "react-hook-form": ">=7.55.0", "sonner": ">=1.7.0", "tailwind-merge": ">=2.6.0", "tailwindcss": ">=3.0.0", "zod": ">=3.0.0" } }, "sha512-mGA0cKsAk0AcoKkxjff2xQOviMrUDDN+9SsRusURP/kiTmoLvWAv8utaPex0GFLHKm1w3BrLBdJRg9B2LkT7Bw=="], "@deco/ui": ["@deco/ui@workspace:packages/ui"], @@ -706,27 +720,27 @@ "@electric-sql/pglite": ["@electric-sql/pglite@0.3.16", "", {}, "sha512-mZkZfOd9OqTMHsK+1cje8OSzfAQcpD7JmILXTl5ahdempjUDdmg4euf1biDex5/LfQIDJ3gvCu6qDgdnDxfJmA=="], - "@embedded-postgres/darwin-arm64": ["@embedded-postgres/darwin-arm64@18.3.0-beta.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VBZ/xRS9Qdzq7MQnus9ScgO+89Ri2mHHqvbSVG3AOFY6xYne65Y9Br+6X1OcKOBWGR/mURBIFfe7CIqVp0Uf3Q=="], + "@embedded-postgres/darwin-arm64": ["@embedded-postgres/darwin-arm64@18.3.0-beta.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Pvrej3Xz5flfyVc9mchVfekrKoTJyvPtM3U0vjuXamZkRKmi+inP2zRmnmzYecIVbr7Zhu82xbsCENMXrwMp9Q=="], - "@embedded-postgres/darwin-x64": ["@embedded-postgres/darwin-x64@18.3.0-beta.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZoS5GpvPWMtijusa+znxT7nMjz535LHi+8VX7CSCPhDi3IyPE5jcebedboyW6fHjUdtnpkTpfCBMwBRYXJL5tA=="], + "@embedded-postgres/darwin-x64": ["@embedded-postgres/darwin-x64@18.3.0-beta.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-MVWe+C47pPoMD9LlIWGQkvZ5Xsu3IBo54CYqnIps/Z1byMIUBNc7y/dZ3mfqEwiCbVDVqirG0CU462xnrSEfKA=="], - "@embedded-postgres/linux-arm": ["@embedded-postgres/linux-arm@18.3.0-beta.16", "", { "os": "linux", "cpu": "arm" }, "sha512-9DlgGPcSq6IrfZoUK8n0omG2HPFTSeWaF2FBrs8RG6Mjs4dMZcWKsViWFKHbZIHH3ikbZAc3pu3c6lHa4cyuDw=="], + "@embedded-postgres/linux-arm": ["@embedded-postgres/linux-arm@18.3.0-beta.17", "", { "os": "linux", "cpu": "arm" }, "sha512-Y2vw7p80PO/Ko7CDm8CCpStnNfMe+oc11e0WZtqAVRjxO6H0oic/ehULhUsWU3mZm5jq7wQAv37VMzf4JN+SFQ=="], - "@embedded-postgres/linux-arm64": ["@embedded-postgres/linux-arm64@18.3.0-beta.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-FJqXZX3heEcTeusXLt6VuPxR//7x2ETtlapNfPfKyanjGrRHEriVFEhEy2nYkcqTecEtdUfUXTbZ1mPTVXEDBw=="], + "@embedded-postgres/linux-arm64": ["@embedded-postgres/linux-arm64@18.3.0-beta.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-hXp7yHJHYWkdjkgF6As8whEHbdYxhBdmXeLpLTw0aiac0O6+0Cbqk3cOR9U+e49oyIpElHVwZUo6OewquSRhSg=="], - "@embedded-postgres/linux-ia32": ["@embedded-postgres/linux-ia32@18.3.0-beta.16", "", { "os": "linux", "cpu": "ia32" }, "sha512-BL0PH7u/t4dfUPU1foggHtbeQZFz5gaedTXGXduze3j46o9Fjyym2afkkLqmgMGBjZXdoo3IFEdiqyFmZsFMzg=="], + "@embedded-postgres/linux-ia32": ["@embedded-postgres/linux-ia32@18.3.0-beta.17", "", { "os": "linux", "cpu": "ia32" }, "sha512-hVUOM+7QxkzAIdN3gewfVwL1EpJIx+0qUiNTD8cMqRtaZyU87e4AFIvBS0UiDJ9xzMTVWr/X24wePtbvIbkopg=="], - "@embedded-postgres/linux-ppc64": ["@embedded-postgres/linux-ppc64@18.3.0-beta.16", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VndcTVmzhEsGWDiJza0j8FiifnX9/lYUEizDB/+/r8BBz45f02XxdLJwaUc+9Y72kLgnfUGYbN9+fyNMQoMPyg=="], + "@embedded-postgres/linux-ppc64": ["@embedded-postgres/linux-ppc64@18.3.0-beta.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-p3/u4YUqSdE2CKUBlC84JGZCi6RnE1fyeLPIIVy2DJUiKtExR5rE3OpDJcVoN40uecYGL+nR4qFocGzDwG1TBw=="], - "@embedded-postgres/linux-x64": ["@embedded-postgres/linux-x64@18.3.0-beta.16", "", { "os": "linux", "cpu": "x64" }, "sha512-SRnH75c2PenxtlJPAdDz3ckVA6/3AB6g/wOJiK33LsR55a2AWjoLrUkPqS0x8PlVAOaQHCzgnq8EDRxrbP1+Dg=="], + "@embedded-postgres/linux-x64": ["@embedded-postgres/linux-x64@18.3.0-beta.17", "", { "os": "linux", "cpu": "x64" }, "sha512-8orSD6NNopSLtjqir4dWQBrj+g8j1eJjWd9mB60A3xbWMzIBIPQpzT7XzbacW9YFSl/DejOLnRXfff+Wr13Tgw=="], - "@embedded-postgres/windows-x64": ["@embedded-postgres/windows-x64@18.3.0-beta.16", "", { "os": "win32", "cpu": "x64" }, "sha512-REupF2FhJMEsXqdeUG+wVWSKpykbruh9Ro5bHG/7RCBuSa/ncJ/8qhtVSI71RTJn1Cb9Shqa/gbnorz+8hat9Q=="], + "@embedded-postgres/windows-x64": ["@embedded-postgres/windows-x64@18.3.0-beta.17", "", { "os": "win32", "cpu": "x64" }, "sha512-kDC5aBsmhWDjeQjj2V4g+Bk+pMeDU27b7l0rBbaKgtt2gsNmCB34ULg/5cqs2kqUKSk/tiGMHKCNE+zQZ+s4rg=="], - "@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], @@ -790,29 +804,27 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], - "@freestyle-sh/with-bun": ["@freestyle-sh/with-bun@0.2.12", "", { "dependencies": { "@freestyle-sh/with-type-js": "0.2.9", "freestyle-sandboxes": "^0.1.43" } }, "sha512-ujpcwye+RwNx76dUeTQeWxR7xDufdFHlRNHfFhW2hC16wSXNk9Nn62klxtjlJ2VAugt969CxiQlD+B5esJMRtw=="], + "@freestyle-sh/with-bun": ["@freestyle-sh/with-bun@0.2.14", "", { "dependencies": { "@freestyle-sh/with-type-js": "0.2.11", "freestyle": "^0.1.46" } }, "sha512-AkFzVdVQTdEfHkyvV/28KtFKW2DUIYA8WXyoMzMDP1512IEQ3JXKCNnG0IVmrdHaNTTyMUh5uzciJqETow9FtA=="], "@freestyle-sh/with-deno": ["@freestyle-sh/with-deno@0.0.4", "", { "dependencies": { "@freestyle-sh/with-type-js": "0.2.9", "freestyle-sandboxes": "^0.1.43" } }, "sha512-tImFxG97Kjel/LAqSIjCIz1mdxlfN5K0eRJDhkQWjWri352jZRIYqHUWIn+0CaMiZxl1ciHc+KQIUXW7ss6h7Q=="], - "@freestyle-sh/with-nodejs": ["@freestyle-sh/with-nodejs@0.2.9", "", { "dependencies": { "@freestyle-sh/with-type-js": "^0.2.9", "freestyle-sandboxes": "^0.1.41" } }, "sha512-LvflGKQcVe3U6AdCVNKTPdZBb2xRoG/67hq9WiOEABI0+ODjiV/lQYZzNRTQvqemvY+cn1ueRFBtfQJ+VW90bw=="], + "@freestyle-sh/with-nodejs": ["@freestyle-sh/with-nodejs@0.2.12", "", { "dependencies": { "@freestyle-sh/with-type-js": "^0.2.11", "freestyle": "^0.1.46" } }, "sha512-jcx7B1uwqOLfSUsU5003eRTiVHuytbjclpePBcv68DUzmqEZ4muil7yQKb3qU+m3pOUNb7UAaJqUfRIIH1QIdQ=="], - "@freestyle-sh/with-type-js": ["@freestyle-sh/with-type-js@0.2.9", "", { "dependencies": { "@freestyle-sh/with-type-js-deps": "^0.2.9", "@freestyle-sh/with-type-run-code": "^0.2.9", "freestyle-sandboxes": "^0.1.28" } }, "sha512-W6rit2s71ekvD+0H+1rIzgovWHAa186mREJWyn4e+y4etW4+SrZ0Q+CF6a+EnkIWwnkPSL164SIbnJHUj3jp8w=="], + "@freestyle-sh/with-type-js": ["@freestyle-sh/with-type-js@0.2.11", "", { "dependencies": { "@freestyle-sh/with-type-js-deps": "^0.2.10", "@freestyle-sh/with-type-run-code": "^0.2.10", "freestyle": "^0.1.46" } }, "sha512-fAC4w1lt+znNjwK3YsXJ5JTzCHCncu/p3rDXmWS/NZynQwByKoysxDst7gfc6zqmJqdG5ayoVexcQOrhErhDRg=="], - "@freestyle-sh/with-type-js-deps": ["@freestyle-sh/with-type-js-deps@0.2.9", "", { "dependencies": { "freestyle-sandboxes": "^0.1.28" } }, "sha512-h4RR3hgTaKY4Gt7TTSgqhhgmq+oEi+3GR/T0Ol1vk00MQXE+iKvLTela5ECeZu1I1sMcKtDkVsOSIztF6Lpu2Q=="], + "@freestyle-sh/with-type-js-deps": ["@freestyle-sh/with-type-js-deps@0.2.10", "", { "dependencies": { "freestyle": "^0.1.46" } }, "sha512-w72dGEGjPDP1Gm9A1Fg/irBW4KUIieKvIRzV9stLgIbybRX836JEEDeVDbgaVUv/FQqKbvJW595asmPriwOLEA=="], - "@freestyle-sh/with-type-run-code": ["@freestyle-sh/with-type-run-code@0.2.9", "", { "dependencies": { "freestyle-sandboxes": "^0.1.28" } }, "sha512-RkuB513J9CsqaAELArTwUsWV8VQR1u9YVvn00LgFy4gBJRBWQc2OOKzipdm7+XG2Sf7CIo6ZYgbVkm+Q3sCe4g=="], + "@freestyle-sh/with-type-run-code": ["@freestyle-sh/with-type-run-code@0.2.10", "", { "dependencies": { "freestyle": "^0.1.46" } }, "sha512-onr8lGnnjfDsmbvgzr1aSLGesS2h4+9RqcDLR0RYnrt6gQ1SZW1j46AeOpjtZpIln7Pbawd0xFK4ypzXWGMgqA=="], "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], - "@hcaptcha/loader": ["@hcaptcha/loader@2.3.0", "", {}, "sha512-i4lnNxKBe+COf3R1nFZEWaZoHIoJjvDgWqvcNrdZq8ehoSNMN6KVZ56dcQ02qKie2h3+BkbkwlJA9DOIuLlK/g=="], - - "@hcaptcha/react-hcaptcha": ["@hcaptcha/react-hcaptcha@1.17.4", "", { "dependencies": { "@babel/runtime": "^7.17.9", "@hcaptcha/loader": "^2.3.0" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-rIvgesG1N7SS9sAYYHFoWm+nXqRrxq7RcA9z2pKkDWV+S1GdfmrTNYA1aPyVWVe3eowphTCwyDJvl97Swwy0mw=="], + "@hcaptcha/react-hcaptcha": ["@hcaptcha/react-hcaptcha@2.0.2", "", {}, "sha512-VbuH6VJ6m3BHmVBHs0fL9t+suZd7PQEqCzqL2BiUbBvbHI3XfvSgdiug2QiEPN8zskbPTIV/FfGPF53JCckrow=="], "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], - "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], @@ -868,13 +880,13 @@ "@inkjs/ui": ["@inkjs/ui@2.0.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-spinners": "^3.0.0", "deepmerge": "^4.3.1", "figures": "^6.1.0" }, "peerDependencies": { "ink": ">=5" } }, "sha512-5+8fJmwtF9UvikzLfph9sA+LS+l37Ij/szQltkuXLOAXwNkBX9innfzh4pLGXIB59vKEQUtc6D4qGvhD7h3pAg=="], - "@instantdb/core": ["@instantdb/core@0.22.158", "", { "dependencies": { "@instantdb/version": "0.22.158", "mutative": "^1.0.10", "uuid": "^11.1.0" } }, "sha512-nq0FddMU7CyJymE5QCWjfcBTTH/Ac6NzL99++92aFhRnt8JilHbcLljUKC9N5k0uVkzRaxAo2uBcP/LIZ8OgYA=="], + "@instantdb/core": ["@instantdb/core@1.0.15", "", { "dependencies": { "@instantdb/version": "1.0.15", "mutative": "^1.0.10", "uuid": "^11.1.0" } }, "sha512-1A4n47U0YLHKhvl0G+CiPGfBynq1cj+NqIVRhUMc/yHYT6rePeRWesxvevNiEJU2sLxOKML6Htcbtdh7jUjSQA=="], - "@instantdb/react": ["@instantdb/react@0.22.158", "", { "dependencies": { "@instantdb/core": "0.22.158", "@instantdb/react-common": "0.22.158", "@instantdb/version": "0.22.158", "eventsource": "^4.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-tMX1qEQAk5euW27dlRwvjg/x9V1AfFGOOl8oo2Ia/NPh+MxL+1gd7HHcIy+JZq95G2IuGoPfqpAZKhYisMdW4w=="], + "@instantdb/react": ["@instantdb/react@1.0.15", "", { "dependencies": { "@instantdb/core": "1.0.15", "@instantdb/react-common": "1.0.15", "@instantdb/version": "1.0.15", "eventsource": "^4.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-d6Jbl10DaUh9Ni8+C0RLoTTHpB0UP9/8wAznimb6Z5dDhFiQ3gQyXpnz81OPXQEpQmEJUscqdGJul0DL17s8/g=="], - "@instantdb/react-common": ["@instantdb/react-common@0.22.158", "", { "dependencies": { "@instantdb/core": "0.22.158", "@instantdb/version": "0.22.158" }, "peerDependencies": { "react": ">=16" } }, "sha512-1ojK7N4JYRZoPlIoGDx+vyIKIq57XVCLBKr+daduCOP0v87eQPgdlLaKfQbxKSOix63Mk/X3r0Izs545W1l6HQ=="], + "@instantdb/react-common": ["@instantdb/react-common@1.0.15", "", { "dependencies": { "@instantdb/core": "1.0.15", "@instantdb/version": "1.0.15" }, "peerDependencies": { "react": ">=16" } }, "sha512-EuDG9DfAx+XLW9UQoGe4lwW7Xos8njIuJp/grm0v7uWvnxlgAqKG+rTKjTDP0P6GfPpRbcZS0Houg0951tQe2g=="], - "@instantdb/version": ["@instantdb/version@0.22.158", "", {}, "sha512-RkO73PcZbYdczZMUGfwM5u0OPCG2Bzs1e/cI9adAPmh/1tSvEpJDCU4j0R9JQnHAoMGJ/XNrGT2DqHfkfBV8Cw=="], + "@instantdb/version": ["@instantdb/version@1.0.15", "", {}, "sha512-xHDT23QK0tKAdxC2Z98mBx+znwnVShYWDsp0juY4xxzTGjrb37qNqRYb+6IIJc1J3GZ+xW/fCIexVK3b0B6fog=="], "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], @@ -906,11 +918,11 @@ "@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@2.0.3", "", { "dependencies": { "consola": "^3.2.3", "detect-libc": "^2.0.0", "https-proxy-agent": "^7.0.5", "node-fetch": "^2.6.7", "nopt": "^8.0.0", "semver": "^7.5.3", "tar": "^7.4.0" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg=="], - "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.4.2", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-xs1qOuyeMOz6t9BXXCXWiukC0/0+48vR08B7uwNdG05wCMnbcNgxiFmdFKDOFbM76qFYFRYlGeRfhfq1U/iZmA=="], + "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.5.0", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-Ph6mcj8u9WBDsBO7s9jKPsyRDz1sBPBJwrk+Ngx09vFInvKsQ6U6kW5amEcGq4dHOreB6DgFrOJk7/fy318YlQ=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], - "@modelcontextprotocol/ext-apps": ["@modelcontextprotocol/ext-apps@1.2.2", "", { "peerDependencies": { "@modelcontextprotocol/sdk": "^1.24.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-qMnhIKb8tyPesl+kZU76Xz9Bi9putCO+LcgvBJ00fDdIniiLZsnQbAeTKoq+sTiYH1rba2Fvj8NPAFxij+gyxw=="], + "@modelcontextprotocol/ext-apps": ["@modelcontextprotocol/ext-apps@1.7.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-gs8rYVx6a8pyCvSpXq7TyVLTERCC94JLrcmJgBs0+3p4jp3iQdJPu1IU+2ovVdFZ1sW8JgmvTkRnxAlIizKINg=="], "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], @@ -918,11 +930,11 @@ "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="], - "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -930,7 +942,7 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@oclif/core": ["@oclif/core@4.9.0", "", { "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", "clean-stack": "^3.0.1", "cli-spinners": "^2.9.2", "debug": "^4.4.3", "ejs": "^3.1.10", "get-package-type": "^0.1.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.3", "minimatch": "^10.2.4", "semver": "^7.7.3", "string-width": "^4.2.3", "supports-color": "^8", "tinyglobby": "^0.2.14", "widest-line": "^3.1.0", "wordwrap": "^1.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-k/ntRgDcUprTT+aaNoF+whk3cY3f9fRD2lkF6ul7JeCUg2MaMXVXZXfbRhJCfsiX51X8/5Pqo0LGdO9SLYXNHg=="], + "@oclif/core": ["@oclif/core@4.10.5", "", { "dependencies": { "ansi-escapes": "^4.3.2", "ansis": "^3.17.0", "clean-stack": "^3.0.1", "cli-spinners": "^2.9.2", "debug": "^4.4.3", "ejs": "^3.1.10", "get-package-type": "^0.1.0", "indent-string": "^4.0.0", "is-wsl": "^2.2.0", "lilconfig": "^3.1.3", "minimatch": "^10.2.5", "semver": "^7.7.3", "string-width": "^4.2.3", "supports-color": "^8", "tinyglobby": "^0.2.14", "widest-line": "^3.1.0", "wordwrap": "^1.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-qcdCF7NrdWPfme6Kr34wwljRCXbCVpL1WVxiNy0Ep6vbWKjxAjFQwuhqkoyL0yjI+KdwtLcOCGn5z2yzdijc8w=="], "@openai/codex": ["@openai/codex@0.105.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.105.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.105.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.105.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.105.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.105.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.105.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-enoNmQs3aOgUhsKYC6kfuKEG0AogS4q01pqcySPwf9zl2r8OcKuMUoLw1v5n4Y7sd3B6qGS7UtTABqbcT5FaMA=="], @@ -950,13 +962,13 @@ "@openrouter/sdk": ["@openrouter/sdk@0.1.27", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RH//L10bSmc81q25zAZudiI4kNkLgxF2E+WU42vghp3N6TEvZ6F0jK7uT3tOxkEn91gzmMw9YVmDENy7SJsajQ=="], - "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg=="], "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.2.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ=="], - "@opentelemetry/core": ["@opentelemetry/core@2.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg=="], + "@opentelemetry/core": ["@opentelemetry/core@2.7.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ=="], "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.207.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-K92RN+kQGTMzFDsCzsYNGqOsXRUnko/Ckk+t/yPJao72MewOLgBUTWVHhebgkNfRCYqDz1v3K0aPT9OJkemvgg=="], @@ -998,11 +1010,11 @@ "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-O5nPwzgg2JHzo59kpQTPUOTzFi0Nv5LxryG27QoXBciX3zWM3z83g+SNOHhiQVYRWFSxoWn1JM2TGD5iNjOwdA=="], - "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw=="], + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.0", "", { "dependencies": { "@opentelemetry/core": "2.7.0", "@opentelemetry/resources": "2.7.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w=="], "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.207.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.207.0", "@opentelemetry/exporter-logs-otlp-http": "0.207.0", "@opentelemetry/exporter-logs-otlp-proto": "0.207.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.207.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.207.0", "@opentelemetry/exporter-prometheus": "0.207.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.207.0", "@opentelemetry/exporter-trace-otlp-http": "0.207.0", "@opentelemetry/exporter-trace-otlp-proto": "0.207.0", "@opentelemetry/exporter-zipkin": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/propagator-b3": "2.2.0", "@opentelemetry/propagator-jaeger": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/sdk-trace-node": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-hnRsX/M8uj0WaXOBvFenQ8XsE8FLVh2uSnn1rkWu4mx+qu7EKGUZvZng6y/95cyzsqOfiaDDr08Ek4jppkIDNg=="], - "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ=="], + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.0", "", { "dependencies": { "@opentelemetry/core": "2.7.0", "@opentelemetry/resources": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-Yg9zEXJB50DLVLpsKPk7NmNqlPlS+OvqhJGh0A8oawIOTPOwlm4eXs9BMJV7L79lvEwI+dWtAj+YjTyddV336A=="], "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.2.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.2.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ=="], @@ -1118,7 +1130,7 @@ "@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], - "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], + "@playwright/test": ["@playwright/test@1.59.1", "", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], @@ -1266,7 +1278,7 @@ "@react-email/column": ["@react-email/column@0.0.14", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg=="], - "@react-email/components": ["@react-email/components@1.0.9", "", { "dependencies": { "@react-email/body": "0.3.0", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", "@react-email/column": "0.0.14", "@react-email/container": "0.0.16", "@react-email/font": "0.0.10", "@react-email/head": "0.0.13", "@react-email/heading": "0.0.16", "@react-email/hr": "0.0.12", "@react-email/html": "0.0.12", "@react-email/img": "0.0.12", "@react-email/link": "0.0.13", "@react-email/markdown": "0.0.18", "@react-email/preview": "0.0.14", "@react-email/render": "2.0.4", "@react-email/row": "0.0.13", "@react-email/section": "0.0.17", "@react-email/tailwind": "2.0.5", "@react-email/text": "0.1.6" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2vi1w423KdjGa9rLUJAq8daTq5xVvB5VHDuI8fRu3/JfqqihzUu5r0bET3qWDw9QpKOIXcZzWO3jN2+yMVtzUw=="], + "@react-email/components": ["@react-email/components@1.0.12", "", { "dependencies": { "@react-email/body": "0.3.0", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", "@react-email/column": "0.0.14", "@react-email/container": "0.0.16", "@react-email/font": "0.0.10", "@react-email/head": "0.0.13", "@react-email/heading": "0.0.16", "@react-email/hr": "0.0.12", "@react-email/html": "0.0.12", "@react-email/img": "0.0.12", "@react-email/link": "0.0.13", "@react-email/markdown": "0.0.18", "@react-email/preview": "0.0.14", "@react-email/render": "2.0.6", "@react-email/row": "0.0.13", "@react-email/section": "0.0.17", "@react-email/tailwind": "2.0.7", "@react-email/text": "0.1.6" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-tH18JhPDWgE+3jnYkzyB6ZrZdfNnEsFe4PwmuXmlOw4NGIysP8wPY5aXZg++pTG9qUabXg1nzX/FGHGkObH8xQ=="], "@react-email/container": ["@react-email/container@0.0.16", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ=="], @@ -1288,13 +1300,13 @@ "@react-email/preview": ["@react-email/preview@0.0.14", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw=="], - "@react-email/render": ["@react-email/render@2.0.4", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g=="], + "@react-email/render": ["@react-email/render@2.0.6", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-xOzaYkH3jLZKqN5MqrTXYnmqBYUnZSVbkxdb5PGGmDcK6sKDVMliaDiSwfXajRC9JtSHTcGc2tmGLHWuCgVpog=="], "@react-email/row": ["@react-email/row@0.0.13", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw=="], "@react-email/section": ["@react-email/section@0.0.17", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w=="], - "@react-email/tailwind": ["@react-email/tailwind@2.0.5", "", { "dependencies": { "tailwindcss": "^4.1.18" }, "peerDependencies": { "@react-email/body": "0.2.1", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", "@react-email/container": "0.0.16", "@react-email/heading": "0.0.16", "@react-email/hr": "0.0.12", "@react-email/img": "0.0.12", "@react-email/link": "0.0.13", "@react-email/preview": "0.0.14", "@react-email/text": "0.1.6", "react": "^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@react-email/body", "@react-email/button", "@react-email/code-block", "@react-email/code-inline", "@react-email/container", "@react-email/heading", "@react-email/hr", "@react-email/img", "@react-email/link", "@react-email/preview"] }, "sha512-7Ey+kiWliJdxPMCLYsdDts8ffp4idlP//w4Ui3q/A5kokVaLSNKG8DOg/8qAuzWmRiGwNQVOKBk7PXNlK5W+sg=="], + "@react-email/tailwind": ["@react-email/tailwind@2.0.7", "", { "dependencies": { "tailwindcss": "^4.1.18" }, "peerDependencies": { "@react-email/body": ">=0", "@react-email/button": ">=0", "@react-email/code-block": ">=0", "@react-email/code-inline": ">=0", "@react-email/container": ">=0", "@react-email/heading": ">=0", "@react-email/hr": ">=0", "@react-email/img": ">=0", "@react-email/link": ">=0", "@react-email/preview": ">=0", "@react-email/text": ">=0", "react": "^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@react-email/body", "@react-email/button", "@react-email/code-block", "@react-email/code-inline", "@react-email/container", "@react-email/heading", "@react-email/hr", "@react-email/img", "@react-email/link", "@react-email/preview"] }, "sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA=="], "@react-email/text": ["@react-email/text@0.1.6", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw=="], @@ -1306,67 +1318,67 @@ "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="], - "@rjsf/core": ["@rjsf/core@6.4.1", "", { "dependencies": { "lodash": "^4.17.23", "lodash-es": "^4.17.23", "markdown-to-jsx": "^8.0.0", "prop-types": "^15.8.1" }, "peerDependencies": { "@rjsf/utils": "^6.4.x", "react": ">=18" } }, "sha512-+QaiSgQnOuO6ghIsohH2u/QcylkN+Da2968a75g/i4oARYJRYVxXDm2u3JR5aXndpMb4t4jTFrYyG8cNIv6oEg=="], + "@rjsf/core": ["@rjsf/core@6.5.1", "", { "dependencies": { "lodash": "^4.18.1", "lodash-es": "^4.18.1", "markdown-to-jsx": "^8.0.0", "prop-types": "^15.8.1" }, "peerDependencies": { "@rjsf/utils": "^6.5.x", "react": ">=18" } }, "sha512-H5GY9OU6wpLXGHaVzd8/1qGZ+zylCA86maD32tMdeOiUIJTmH6+VeheRx4Wr6OnprxbD/ybkEH48vIqS68wwxg=="], - "@rjsf/shadcn": ["@rjsf/shadcn@6.4.1", "", { "dependencies": { "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.0", "@react-icons/all-files": "^4.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "lodash": "^4.17.23", "lodash-es": "^4.17.23", "lucide-react": "^0.548.0", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", "uuid": "^13.0.0" }, "peerDependencies": { "@rjsf/core": "^6.4.x", "@rjsf/utils": "^6.4.x", "react": ">=18" } }, "sha512-WzwXW3XY7K1jo9XrBv6M41ScdHrnQDKpSxip5i1N6xCgEE6hiyX+wn7pDO689OoidvL3lWQmtnoqMdcoJvEWjw=="], + "@rjsf/shadcn": ["@rjsf/shadcn@6.5.1", "", { "dependencies": { "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.0", "@react-icons/all-files": "^4.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "lodash": "^4.18.1", "lodash-es": "^4.18.1", "lucide-react": "^0.548.0", "tailwind-merge": "^3.5.0", "tailwindcss-animate": "^1.0.7", "uuid": "^13.0.0" }, "peerDependencies": { "@rjsf/core": "^6.5.x", "@rjsf/utils": "^6.5.x", "react": ">=18" } }, "sha512-6azuQMvMwZvHh2qvrT5vHcS4lqpjvf7quRIRdB7KQTMzElj+kZSuew451MJsOl0YPKysMTGKpCDFEUCun6io9Q=="], - "@rjsf/utils": ["@rjsf/utils@6.4.1", "", { "dependencies": { "@x0k/json-schema-merge": "^1.0.2", "fast-uri": "^3.1.0", "jsonpointer": "^5.0.1", "lodash": "^4.17.23", "lodash-es": "^4.17.23", "react-is": "^18.3.1" }, "peerDependencies": { "react": ">=18" } }, "sha512-5NL3jwt3rIS5/WRTrKt++y40FS/ScKGVwYJ3jIrHSQHSwBdLnd4cHf2zcnA97L1Klj8I6tvS/ugh+blf/Diwuw=="], + "@rjsf/utils": ["@rjsf/utils@6.5.1", "", { "dependencies": { "@x0k/json-schema-merge": "^1.0.3", "fast-uri": "^3.1.0", "jsonpointer": "^5.0.1", "lodash": "^4.18.1", "lodash-es": "^4.18.1", "react-is": "^18.3.1" }, "peerDependencies": { "react": ">=18" } }, "sha512-c+x2VJNEp0BsamxX+Ryy9sEmwJ/7V9WFsVWjhADwyEU53r7DaVd7a7hmtx0bz464kJ8oJYZ6XghrmXXH2y7l8g=="], - "@rjsf/validator-ajv8": ["@rjsf/validator-ajv8@6.4.1", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^2.1.1", "lodash": "^4.17.23", "lodash-es": "^4.17.23" }, "peerDependencies": { "@rjsf/utils": "^6.4.x" } }, "sha512-Gx28sRIV7E4CYs2c7BxOGLX44p5IlJE+IaD7GbVk1S+6TxDATqFBSYYZukLB+/vNk3urpndQMreQLKW3W7POHQ=="], + "@rjsf/validator-ajv8": ["@rjsf/validator-ajv8@6.5.1", "", { "dependencies": { "ajv": "^8.18.0", "ajv-formats": "^2.1.1", "lodash": "^4.18.1", "lodash-es": "^4.18.1" }, "peerDependencies": { "@rjsf/utils": "^6.5.x" } }, "sha512-0EfPRRe0ia3dvcqWt8vY1sUGB1vb4e+3GfivBXuJZmSBtw+zlHwyClAJnPW/4Qde2g21u0k344/2miP9DZJmCw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.2", "", { "os": "android", "cpu": "arm" }, "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.2", "", { "os": "android", "cpu": "arm64" }, "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.2", "", { "os": "linux", "cpu": "arm" }, "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.2", "", { "os": "linux", "cpu": "none" }, "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.2", "", { "os": "none", "cpu": "arm64" }, "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], @@ -1392,75 +1404,73 @@ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], - "@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="], - "@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw=="], "@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.2.3", "", { "dependencies": { "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw=="], - "@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="], + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.17", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ=="], - "@smithy/core": ["@smithy/core@3.23.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w=="], + "@smithy/core": ["@smithy/core@3.23.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.14", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg=="], - "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="], + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], - "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="], + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.14", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ=="], - "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="], + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA=="], - "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="], + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.14", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw=="], - "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="], + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.14", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.17", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw=="], - "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.13", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g=="], + "@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.15", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA=="], - "@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="], + "@smithy/hash-node": ["@smithy/hash-node@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g=="], - "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw=="], + "@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ=="], - "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="], + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw=="], "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="], - "@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="], + "@smithy/md5-js": ["@smithy/md5-js@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA=="], - "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="], + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.14", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw=="], - "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="], + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.32", "", { "dependencies": { "@smithy/core": "^3.23.17", "@smithy/middleware-serde": "^4.2.20", "@smithy/node-config-provider": "^4.3.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" } }, "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q=="], - "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.44", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA=="], + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.5.5", "", { "dependencies": { "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", "@smithy/service-error-classification": "^4.3.0", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.4", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-wnYOpB5vATFKWrY2Z9Alb0KhjZI6AbzU6Fbz3Hq2GnURdRYWB4q+qWivQtSTwXcmWUA3MZ6krfwL6Cq5MAbxsA=="], - "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.15", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg=="], + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.20", "", { "dependencies": { "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ=="], - "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="], + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA=="], - "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="], + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.14", "", { "dependencies": { "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.6.1", "", { "dependencies": { "@smithy/protocol-http": "^5.3.14", "@smithy/querystring-builder": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg=="], - "@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="], + "@smithy/property-provider": ["@smithy/property-provider@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ=="], - "@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="], + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ=="], - "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="], + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A=="], - "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="], + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw=="], - "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="], + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.3.0", "", { "dependencies": { "@smithy/types": "^4.14.1" } }, "sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A=="], - "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="], + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.9", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.14", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA=="], - "@smithy/smithy-client": ["@smithy/smithy-client@4.12.7", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ=="], + "@smithy/smithy-client": ["@smithy/smithy-client@4.12.13", "", { "dependencies": { "@smithy/core": "^3.23.17", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-stack": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", "@smithy/util-stream": "^4.5.25", "tslib": "^2.6.2" } }, "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA=="], - "@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], + "@smithy/types": ["@smithy/types@4.14.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg=="], - "@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="], + "@smithy/url-parser": ["@smithy/url-parser@4.2.14", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ=="], "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="], @@ -1472,65 +1482,65 @@ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="], - "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.43", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ=="], + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.49", "", { "dependencies": { "@smithy/property-provider": "^4.2.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw=="], - "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.47", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ=="], + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.54", "", { "dependencies": { "@smithy/config-resolver": "^4.4.17", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw=="], - "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="], + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.4.2", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg=="], "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="], - "@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="], + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.14", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw=="], - "@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="], + "@smithy/util-retry": ["@smithy/util-retry@4.3.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.3.0", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-FY1UQQ1VFmMwiYp1GVS4MeaGD5O0blLNYK0xCRHU+mJgeoH/hSY8Ld8sJWKQ6uznkh14HveRGQJncgPyNl9J+A=="], - "@smithy/util-stream": ["@smithy/util-stream@4.5.20", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw=="], + "@smithy/util-stream": ["@smithy/util-stream@4.5.25", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.6.1", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA=="], "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="], "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="], - "@smithy/util-waiter": ["@smithy/util-waiter@4.2.13", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ=="], + "@smithy/util-waiter": ["@smithy/util-waiter@4.2.16", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ=="], "@smithy/uuid": ["@smithy/uuid@1.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="], - "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], + "@speed-highlight/core": ["@speed-highlight/core@1.2.15", "", {}, "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.4", "@tailwindcss/oxide-darwin-arm64": "4.2.4", "@tailwindcss/oxide-darwin-x64": "4.2.4", "@tailwindcss/oxide-freebsd-x64": "4.2.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", "@tailwindcss/oxide-linux-x64-musl": "4.2.4", "@tailwindcss/oxide-wasm32-wasi": "4.2.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" } }, "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q=="], - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="], + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.4", "", { "os": "android", "cpu": "arm64" }, "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g=="], - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="], + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg=="], - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="], + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg=="], - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="], + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw=="], - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="], + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA=="], - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="], + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw=="], - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="], + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g=="], - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="], + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA=="], - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="], + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA=="], - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="], + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.4", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw=="], - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="], + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ=="], - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="], + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw=="], "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], + "@tailwindcss/vite": ["@tailwindcss/vite@4.2.4", "", { "dependencies": { "@tailwindcss/node": "4.2.4", "@tailwindcss/oxide": "4.2.4", "tailwindcss": "4.2.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw=="], "@tanstack/history": ["@tanstack/history@1.139.0", "", {}, "sha512-l6wcxwDBeh/7Dhles23U1O8lp9kNJmAb2yNjekR6olZwCRNAVA8TCXlVCrueELyFlYZqvQkh0ofxnzG62A1Kkg=="], @@ -1542,13 +1552,13 @@ "@tanstack/react-store": ["@tanstack/react-store@0.8.1", "", { "dependencies": { "@tanstack/store": "0.8.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.22", "", { "dependencies": { "@tanstack/virtual-core": "3.13.22" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-EaOrBBJLi3M0bTMQRjGkxLXRw7Gizwntoy5E2Q2UnSbML7Mo2a1P/Hfkw5tw9FLzK62bj34Jl6VNbQfRV6eJcA=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.24", "", { "dependencies": { "@tanstack/virtual-core": "3.14.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg=="], "@tanstack/router-core": ["@tanstack/router-core@1.139.7", "", { "dependencies": { "@tanstack/history": "1.139.0", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.0", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-mqgsJi4/B2Jo6PXRUs1AsWA+06nqiqVZe1aXioA3vR6PesNeKUSXWfmIoYF6wOx3osiV0BnwB1JCBrInCOQSWA=="], "@tanstack/store": ["@tanstack/store@0.8.1", "", {}, "sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.22", "", {}, "sha512-isuUGKsc5TAPDoHSbWTbl1SCil54zOS2MiWz/9GCWHPUQOvNTQx8qJEWC7UWR0lShhbK0Lmkcf0SZYxvch7G3g=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.14.0", "", {}, "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q=="], "@tiptap/core": ["@tiptap/core@3.20.2", "", { "peerDependencies": { "@tiptap/pm": "^3.20.2" } }, "sha512-zKW4LqZt+aNdvz9o4R0/j+D+gfhwzuFItwh7wbqz8g8bWi0jaV95VybeVFVKeg/KGTc3sAa4mm+hGgvgrY+Gvg=="], @@ -1556,7 +1566,7 @@ "@tiptap/extension-bold": ["@tiptap/extension-bold@3.20.2", "", { "peerDependencies": { "@tiptap/core": "^3.20.2" } }, "sha512-NLqh6ewHcDDPveTCL2f6BQcsDI5lubNjiyzvuYr0ZO9AV5Fqw8TkYwoKNijiYlgGRtm+pZLhMnf45gbLJQoymg=="], - "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.20.3", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "^3.20.3", "@tiptap/pm": "^3.20.3" } }, "sha512-21sVeo9ixzK44W6abCI3tbX3aSa9zwounqTkPArGCmk/imI9DQyo8JaZ+36KnnpWFJiKbiikMLhqrEdvV3Wj6w=="], + "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.22.4", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-v4pux5Ql3THAEjaLMY4ldtdy/Xy2qU7PJLBkq8ugLp8qicaKC+tpqxp6sGif4vLIjz7Ap5hurRbTNbXzszyyHA=="], "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.20.2", "", { "peerDependencies": { "@tiptap/extension-list": "^3.20.2" } }, "sha512-LHmp945at3YYl2VPIg0bopyJioi52xK+YRurOz8A440EgCdnAkFa0UDGHxK/e4Y0R2y3xbPl+VBl3HzZjXPFuw=="], @@ -1568,7 +1578,7 @@ "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.20.2", "", { "peerDependencies": { "@tiptap/extensions": "^3.20.2" } }, "sha512-LpBZOOgTrFWkYneOWOd0xyB7HUGIZqrgEhL+Beohzxkx63uNRC3PxFAAXhju6wxcvQ49e/WMg++Z8EDwHb6f2Q=="], - "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.20.3", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.20.3", "@tiptap/pm": "^3.20.3" } }, "sha512-vojKVspzxlnC3DjVKhfbYkijNDDGzxHTA13Y6/J0cOJMGmx+M/QO05gjYKZMyw0JpmkhT9Rbcsg1bElwuI/SMw=="], + "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.22.4", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.22.4", "@tiptap/pm": "3.22.4" } }, "sha512-DFuyYxgaZPgxum5z1yvJPbfYCvDdO8geXsdyqt0qYYdiat3aGE4ncJhiLRIFDhSHBhaZg5eCgu/YPYAN6jZnrA=="], "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.20.2", "", { "peerDependencies": { "@tiptap/extensions": "^3.20.2" } }, "sha512-IfQuD5XctZa+Xxy3mdjo9NTYbiMFqGPuzyh2ypHUqyuvIwxOIRhxTFaCijOGVYn1g3BH8nzGMhZ5rnZ48zIb6Q=="], @@ -1630,7 +1640,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], @@ -1650,7 +1660,7 @@ "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], @@ -1680,11 +1690,11 @@ "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], - "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], - "@types/pg": ["@types/pg@8.18.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q=="], + "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -1704,19 +1714,19 @@ "@untitledui/icons": ["@untitledui/icons@0.0.19", "", { "peerDependencies": { "react": ">= 16" } }, "sha512-cZA7BBE5+piNpzO9CiXypbmGYSDqnT3WwHn6/TU7cUuYl7oUlwIn0AwW/kYDDkmJMPu/wRlnJDR1/OiXd4nqew=="], - "@vercel/nft": ["@vercel/nft@1.3.2", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-HC8venRc4Ya7vNeBsJneKHHMDDWpQie7VaKhAIOst3MKO+DES+Y/SbzSp8mFkD7OzwAE2HhHkeSuSmwS20mz3A=="], + "@vercel/nft": ["@vercel/nft@1.5.0", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA=="], - "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], "@wojtekmaj/react-recaptcha-v3": ["@wojtekmaj/react-recaptcha-v3@0.1.4", "", { "dependencies": { "warning": "^4.0.0" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-zszMOdgI+y1Dz3496pRFr3t68n9+OmX/puLQNnOBDC7WrjM+nOKGyjIMCTe+3J14KDvzcxETeiglyDMGl0Yh/Q=="], - "@x0k/json-schema-merge": ["@x0k/json-schema-merge@1.0.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-1734qiJHNX3+cJGDMMw2yz7R+7kpbAtl5NdPs1c/0gO5kYT6s4dMbLXiIfpZNsOYhGZI3aH7FWrj4Zxz7epXNg=="], + "@x0k/json-schema-merge": ["@x0k/json-schema-merge@1.0.3", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-lerJC4sI9CNUQWdff3PnU1YJOqazD6TjMcvxZIPXUBjn4j1cUiXE0LvzhMnGYzKKr271TkvXJtH7gEwksrtn+w=="], "@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="], @@ -1734,13 +1744,13 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="], + "ai": ["ai@6.0.168", "", { "dependencies": { "@ai-sdk/gateway": "3.0.104", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ=="], "ai-sdk-provider-claude-code": ["ai-sdk-provider-claude-code@3.4.4", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "@anthropic-ai/claude-agent-sdk": "^0.2.63" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-iHcup5SHh4Tul1RIi9J+bnpngen8WX66yC3lsz1YlbtwAmRhUEzZUuGKzmFGIN8Pmx9uQrerGfLJdbFxIxKkyw=="], "ai-sdk-provider-codex-cli": ["ai-sdk-provider-codex-cli@1.1.0", "", { "dependencies": { "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.1", "jsonc-parser": "^3.3.1" }, "optionalDependencies": { "@openai/codex": "^0.105.0" }, "peerDependencies": { "zod": "^3.0.0 || ^4.0.0" } }, "sha512-l1ap+RAwCpdwEaLmB2cpQ4br+h2ut2IR0IvI88QvUMAAk5c3h2dSnx+vYZhwsOt1th3CS7FUQmvCTcnxqYshWg=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -1768,7 +1778,7 @@ "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], - "asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="], + "asn1js": ["asn1js@3.0.10", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.5", "tslib": "^2.8.1" } }, "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -1808,11 +1818,11 @@ "base-64": ["base-64@1.0.0", "", {}, "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Q+rUQ7Uz8AHM7DEaNdwvfFCTq7a43lNTzuS94eiWqwyxfV/wJv+oUivef51T91mmRY4d4A1u9rcSvkeufCVXlA=="], "better-auth": ["better-auth@1.4.5", "", { "dependencies": { "@better-auth/core": "1.4.5", "@better-auth/telemetry": "1.4.5", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.4", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "ms": "4.0.0-nightly.202508271359", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@sveltejs/kit", "@tanstack/react-start", "next", "react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-pHV2YE0OogRHvoA6pndHXCei4pcep/mjY7psSaHVrRgjBtumVI68SV1g9U9XPRZ4KkoGca9jfwuv+bB2UILiFw=="], - "better-call": ["better-call@1.1.5", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-nQJ3S87v6wApbDwbZ++FrQiSiVxWvZdjaO+2v6lZJAG2WWggkB2CziUDjPciz3eAt9TqfRursIQMZIcpkBnvlw=="], + "better-call": ["better-call@2.0.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-QqSKtfJD/ZzQdlm7BTUxT9RCA0AxcrZEMyU/yl7/uoFDoR7YCTdc555xQXjReo75M6/xkskPawPdhbn3fge4Cg=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], @@ -1828,13 +1838,13 @@ "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], - "brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], - "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], @@ -1848,7 +1858,7 @@ "camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001790", "", {}, "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1916,7 +1926,7 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -1926,13 +1936,13 @@ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], + "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="], "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], - "core-js": ["core-js@3.48.0", "", {}, "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ=="], + "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], @@ -1992,7 +2002,7 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], "degit": ["degit@2.8.4", "", { "bin": { "degit": "degit" } }, "sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng=="], @@ -2004,19 +2014,17 @@ "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], - "detect-europe-js": ["detect-europe-js@0.1.2", "", {}, "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="], - "devalue": ["devalue@5.6.4", "", {}, "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA=="], + "devalue": ["devalue@5.7.1", "", {}, "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], @@ -2032,7 +2040,7 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="], + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], @@ -2044,11 +2052,11 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.344", "", {}, "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg=="], "elen": ["elen@1.0.10", "", {}, "sha512-ZL799/V/kzxYJ6Wlfktreq6qQWfGc3VkGUQJW5lZQ8/MhsQiKTAwERPfhEwIsV2movRGe2DfV7H2MjRw76Z7Wg=="], - "embedded-postgres": ["embedded-postgres@18.3.0-beta.16", "", { "dependencies": { "async-exit-hook": "^2.0.1", "pg": "^8.7.3" }, "optionalDependencies": { "@embedded-postgres/darwin-arm64": "^18.3.0-beta.16", "@embedded-postgres/darwin-x64": "^18.3.0-beta.16", "@embedded-postgres/linux-arm": "^18.3.0-beta.16", "@embedded-postgres/linux-arm64": "^18.3.0-beta.16", "@embedded-postgres/linux-ia32": "^18.3.0-beta.16", "@embedded-postgres/linux-ppc64": "^18.3.0-beta.16", "@embedded-postgres/linux-x64": "^18.3.0-beta.16", "@embedded-postgres/windows-x64": "^18.3.0-beta.16" } }, "sha512-iCe14miQhWUr1MqER2NUnFlylXk2+8cILCqTCo7gvi2tKq4jZM0Mofy+e7ZXOy5mILh+8EdvJ1USobFK33SJow=="], + "embedded-postgres": ["embedded-postgres@18.3.0-beta.17", "", { "dependencies": { "async-exit-hook": "^2.0.1", "pg": "^8.7.3" }, "optionalDependencies": { "@embedded-postgres/darwin-arm64": "^18.3.0-beta.17", "@embedded-postgres/darwin-x64": "^18.3.0-beta.17", "@embedded-postgres/linux-arm": "^18.3.0-beta.17", "@embedded-postgres/linux-arm64": "^18.3.0-beta.17", "@embedded-postgres/linux-ia32": "^18.3.0-beta.17", "@embedded-postgres/linux-ppc64": "^18.3.0-beta.17", "@embedded-postgres/linux-x64": "^18.3.0-beta.17", "@embedded-postgres/windows-x64": "^18.3.0-beta.17" } }, "sha512-1biFWyuPVtAV5S9RBgcr4PGuAdNL9WhnNZVQ5Arp3gsB24Ci9X9s/8Z7RFYFSc6tJWcj9kzF55YcDAcr3jLUbQ=="], "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], @@ -2062,7 +2070,7 @@ "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], + "enhanced-resolve": ["enhanced-resolve@5.21.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -2080,7 +2088,7 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="], + "es-toolkit": ["es-toolkit@1.46.0", "", {}, "sha512-IToJ6ct9OLl5zz6WsC/1vZEwfSZ7Myil+ygl5Tf30Xjn9AEkzNB4kqp2G7VUJKF1DtTx/ra5M5KLlXvzOg51BA=="], "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], @@ -2116,11 +2124,11 @@ "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="], + "express-rate-limit": ["express-rate-limit@8.4.0", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-gDK8yiqKxrGta+3WtON59arrrw6GLmadA1qoFgYXzdcch8fmKDID2XqO8itsi3f1wufXYPT51387dN6cvVBS3Q=="], "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], @@ -2134,7 +2142,7 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - "fast-xml-builder": ["fast-xml-builder@1.1.3", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg=="], + "fast-xml-builder": ["fast-xml-builder@1.1.5", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA=="], "fast-xml-parser": ["fast-xml-parser@5.4.2", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ=="], @@ -2172,6 +2180,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "freestyle": ["freestyle@0.1.49", "", { "optionalDependencies": { "dotenv": "^17.3.1", "glob": "^13.0.0", "yargs": "^18.0.0" }, "bin": { "freestyle": "cli.mjs", "freestyle-sandboxes": "cli.mjs" } }, "sha512-lRjWNhk0nPjR3rto4D7cA03uvKMCo1U3V5XJhY6RKSyIbplWRlDjmSA45rA060svNWBU6HSHNVnR4cYHpSLeMg=="], + "freestyle-sandboxes": ["freestyle-sandboxes@0.1.46", "", { "optionalDependencies": { "dotenv": "^17.3.1", "glob": "^13.0.0", "yargs": "^18.0.0" }, "bin": { "freestyle": "cli.mjs", "freestyle-sandboxes": "cli.mjs" } }, "sha512-iQ+nd9xcqALozY+XV0X4eIl/TlpWCvPSrSBB3WA9vRu/mVkUO2vTcItu3FJwB1j4bQry/RMNNQrlItFmZu+bcg=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -2198,7 +2208,7 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], "git-diff": ["git-diff@2.0.6", "", { "dependencies": { "chalk": "^2.3.2", "diff": "^3.5.0", "loglevel": "^1.6.1", "shelljs": "^0.8.1", "shelljs.exec": "^1.1.7" } }, "sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA=="], @@ -2216,7 +2226,7 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "h3": ["h3@1.15.6", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-oi15ESLW5LRthZ+qPCi5GNasY/gvynSKUQxgiovrY63bPAtG59wtM+LSrlcwvOHAXzGrXVLnI97brbkdPF9WoQ=="], + "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -2224,7 +2234,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], @@ -2256,7 +2266,7 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], - "hono": ["hono@4.12.7", "", {}, "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw=="], + "hono": ["hono@4.12.15", "", {}, "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg=="], "hpagent": ["hpagent@1.2.0", "", {}, "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA=="], @@ -2340,15 +2350,13 @@ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "is-standalone-pwa": ["is-standalone-pwa@0.1.1", "", {}, "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g=="], - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], - "isbot": ["isbot@5.1.36", "", {}, "sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ=="], + "isbot": ["isbot@5.1.39", "", {}, "sha512-obH0yYahGXdzNxo+djmHhBYThUKDkz565cxkIlt2L9hXfv1NlaLKoDBHo6KxXsYrIXx2RK3x5vY36CfZcobxEw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -2358,7 +2366,7 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], @@ -2384,7 +2392,7 @@ "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="], @@ -2392,9 +2400,9 @@ "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "knip": ["knip@5.86.0", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-tGpRCbP+L+VysXnAp1bHTLQ0k/SdC3M3oX18+Cpiqax1qdS25iuCPzpK8LVmAKARZv0Ijri81Wq09Rzk0JTl+Q=="], + "knip": ["knip@5.88.1", "", { "dependencies": { "@nodelib/fs.walk": "^1.2.3", "fast-glob": "^3.3.3", "formatly": "^0.3.0", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-resolver": "^11.19.1", "picocolors": "^1.1.1", "picomatch": "^4.0.1", "smol-toml": "^1.5.2", "strip-json-comments": "5.0.3", "unbash": "^2.2.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "peerDependencies": { "@types/node": ">=18", "typescript": ">=5.0.4 <7" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-tpy5o7zu1MjawVkLPuahymVJekYY3kYjvzcoInhIchgePxTlo+api90tBv2KfhAIe5uXh+mez1tAfmbv8/TiZg=="], - "kysely": ["kysely@0.28.12", "", {}, "sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw=="], + "kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="], "kysely-codegen": ["kysely-codegen@0.15.0", "", { "dependencies": { "chalk": "4.1.2", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "git-diff": "^2.0.6", "micromatch": "^4.0.5", "minimist": "^1.2.8" }, "peerDependencies": { "@libsql/kysely-libsql": "^0.3.0", "@tediousjs/connection-string": "^0.5.0", "better-sqlite3": ">=7.6.2", "kysely": "^0.27.0", "kysely-bun-worker": "^0.5.3", "mysql2": "^2.3.3 || ^3.0.0", "pg": "^8.8.0", "tarn": "^3.0.0", "tedious": "^16.6.0 || ^17.0.0" }, "optionalPeers": ["@libsql/kysely-libsql", "@tediousjs/connection-string", "better-sqlite3", "kysely-bun-worker", "mysql2", "pg", "tarn", "tedious"], "bin": { "kysely-codegen": "dist/cli/bin.js" } }, "sha512-LPta2nQOyoEPDQ3w/Gsplc+2iyZPAsGvtWoS21VzOB0NDQ0B38Xy1gS8WlbGef542Zdw2eLJHxekud9DzVdNRw=="], @@ -2402,29 +2410,29 @@ "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], - "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], @@ -2436,9 +2444,9 @@ "load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="], - "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], @@ -2452,7 +2460,7 @@ "lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="], - "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + "lru-cache": ["lru-cache@11.3.5", "", {}, "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw=="], "lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="], @@ -2598,9 +2606,9 @@ "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "miniflare": ["miniflare@4.20260312.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260312.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-pieP2rfXynPT6VRINYaiHe/tfMJ4c5OIhqRlIdLF6iZ9g5xgpEmvimvIgMpgAdDJuFlrLcwDUi8MfAo2R6dt/w=="], + "miniflare": ["miniflare@4.20260424.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.24.8", "workerd": "1.20260424.1", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-B6MKBBd5TJ19daUc3Ae9rWctn1nDA/VCXykXfCsp9fTxyfGxnZY27tJs1caxgE9MWEMMKGbGHouqVtgKbKGxmw=="], - "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -2608,7 +2616,7 @@ "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - "mlly": ["mlly@1.8.1", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ=="], + "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], @@ -2622,9 +2630,9 @@ "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanoid": ["nanoid@5.1.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw=="], - "nanostores": ["nanostores@1.1.1", "", {}, "sha512-EYJqS25r2iBeTtGQCHidXl1VfZ1jXM7Q04zXJOrMlxVVmD0ptxJaNux92n1mJ7c5lN3zTq12MhH/8x59nP+qmg=="], + "nanostores": ["nanostores@1.3.0", "", {}, "sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA=="], "nats": ["nats@2.29.3", "", { "dependencies": { "nkeys.js": "1.1.0" } }, "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA=="], @@ -2648,7 +2656,7 @@ "node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="], - "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], "node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="], @@ -2674,9 +2682,9 @@ "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], + "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], - "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], + "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], "openid-client": ["openid-client@6.8.3", "", { "dependencies": { "jose": "^6.2.2", "oauth4webapi": "^3.8.5" } }, "sha512-AoY/NaN9esS3+xvHInFSK0g3skSfeE0uqQAKRj4rB6/GsBIvzwTUaYo9+HcqpKIaP0dP85p5W07hayKgS4GAeA=="], @@ -2706,7 +2714,7 @@ "patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="], - "path-expression-matcher": ["path-expression-matcher@1.1.3", "", {}, "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ=="], + "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], @@ -2744,7 +2752,7 @@ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], @@ -2752,11 +2760,11 @@ "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], - "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], + "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], - "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="], "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], @@ -2770,7 +2778,7 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], @@ -2780,7 +2788,7 @@ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "prosemirror-changeset": ["prosemirror-changeset@2.4.0", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng=="], + "prosemirror-changeset": ["prosemirror-changeset@2.4.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw=="], "prosemirror-collab": ["prosemirror-collab@1.3.1", "", { "dependencies": { "prosemirror-state": "^1.0.0" } }, "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ=="], @@ -2798,7 +2806,7 @@ "prosemirror-markdown": ["prosemirror-markdown@1.13.4", "", { "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0" } }, "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw=="], - "prosemirror-menu": ["prosemirror-menu@1.3.0", "", { "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg=="], + "prosemirror-menu": ["prosemirror-menu@1.3.2", "", { "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "sha512-6VgUJTYod0nMBlCaYJGhXGLu7Gt4AvcwcOq0YfJCY/6Uh+3S7UsWhpy6rJFCBFOmonq1hD8KyWOtZhkppd4YPg=="], "prosemirror-model": ["prosemirror-model@1.25.4", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA=="], @@ -2812,9 +2820,9 @@ "prosemirror-trailing-node": ["prosemirror-trailing-node@3.0.0", "", { "dependencies": { "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { "prosemirror-model": "^1.22.1", "prosemirror-state": "^1.4.2", "prosemirror-view": "^1.33.8" } }, "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ=="], - "prosemirror-transform": ["prosemirror-transform@1.11.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw=="], + "prosemirror-transform": ["prosemirror-transform@1.12.0", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="], - "prosemirror-view": ["prosemirror-view@1.41.6", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg=="], + "prosemirror-view": ["prosemirror-view@1.41.8", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], "protobufjs": ["protobufjs@8.0.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw=="], @@ -2830,7 +2838,7 @@ "qr.js": ["qr.js@0.0.0", "", {}, "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="], - "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], + "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -2844,17 +2852,17 @@ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + "react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], "react-async-script": ["react-async-script@1.2.0", "", { "dependencies": { "hoist-non-react-statics": "^3.3.0", "prop-types": "^15.5.0" }, "peerDependencies": { "react": ">=16.4.1" } }, "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q=="], "react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="], - "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + "react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], "react-google-recaptcha": ["react-google-recaptcha@3.1.0", "", { "dependencies": { "prop-types": "^15.5.0", "react-async-script": "^1.2.0" }, "peerDependencies": { "react": ">=16.4.1" } }, "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg=="], - "react-hook-form": ["react-hook-form@7.71.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA=="], + "react-hook-form": ["react-hook-form@7.73.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-VAfVYOPcx3piiEVQy95vyFmBwbVUsP/AUIN+mpFG8h11yshDd444nn0VyfaGWSRnhOLVgiDu7HIuBtAIzxn9dA=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], @@ -2942,7 +2950,7 @@ "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], @@ -2962,7 +2970,7 @@ "rfc4648": ["rfc4648@1.5.4", "", {}, "sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg=="], - "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "rollup": ["rollup@4.60.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.2", "@rollup/rollup-android-arm64": "4.60.2", "@rollup/rollup-darwin-arm64": "4.60.2", "@rollup/rollup-darwin-x64": "4.60.2", "@rollup/rollup-freebsd-arm64": "4.60.2", "@rollup/rollup-freebsd-x64": "4.60.2", "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", "@rollup/rollup-linux-arm-musleabihf": "4.60.2", "@rollup/rollup-linux-arm64-gnu": "4.60.2", "@rollup/rollup-linux-arm64-musl": "4.60.2", "@rollup/rollup-linux-loong64-gnu": "4.60.2", "@rollup/rollup-linux-loong64-musl": "4.60.2", "@rollup/rollup-linux-ppc64-gnu": "4.60.2", "@rollup/rollup-linux-ppc64-musl": "4.60.2", "@rollup/rollup-linux-riscv64-gnu": "4.60.2", "@rollup/rollup-linux-riscv64-musl": "4.60.2", "@rollup/rollup-linux-s390x-gnu": "4.60.2", "@rollup/rollup-linux-x64-gnu": "4.60.2", "@rollup/rollup-linux-x64-musl": "4.60.2", "@rollup/rollup-openbsd-x64": "4.60.2", "@rollup/rollup-openharmony-arm64": "4.60.2", "@rollup/rollup-win32-arm64-msvc": "4.60.2", "@rollup/rollup-win32-ia32-msvc": "4.60.2", "@rollup/rollup-win32-x64-gnu": "4.60.2", "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ=="], "rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], @@ -2978,49 +2986,49 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "samlify": ["samlify@2.11.0", "", { "dependencies": { "@authenio/xml-encryption": "^2.0.2", "@xmldom/xmldom": "^0.8.11", "camelcase": "^9.0.0", "node-rsa": "^1.1.1", "xml": "^1.0.1", "xml-crypto": "^6.1.2", "xml-escape": "^1.1.0", "xpath": "^0.0.34" } }, "sha512-1C9ukjlf0rRsuyqdzztqikdItqa33j9NCCDZgeBiWk0etU6vxNB+SWJKW4Flk07ZlhXeev/twALEKrPhIAyfDg=="], + "samlify": ["samlify@2.12.0", "", { "dependencies": { "@authenio/xml-encryption": "^2.0.2", "@xmldom/xmldom": "^0.8.11", "node-rsa": "^1.1.1", "xml": "^1.0.1", "xml-crypto": "^6.1.2", "xml-escape": "^1.1.0", "xpath": "^0.0.34" } }, "sha512-ewGsHyY4kInDH0BfprlAZ1rHpH1jBmbqYiXDbuI3t1Y8h71gqEt4Z7jdCFyPHFR8jItJkbdckTijUZGg14CDlg=="], - "sass": ["sass@1.98.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A=="], + "sass": ["sass@1.99.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q=="], - "sass-embedded": ["sass-embedded@1.98.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.1.5", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.98.0", "sass-embedded-android-arm": "1.98.0", "sass-embedded-android-arm64": "1.98.0", "sass-embedded-android-riscv64": "1.98.0", "sass-embedded-android-x64": "1.98.0", "sass-embedded-darwin-arm64": "1.98.0", "sass-embedded-darwin-x64": "1.98.0", "sass-embedded-linux-arm": "1.98.0", "sass-embedded-linux-arm64": "1.98.0", "sass-embedded-linux-musl-arm": "1.98.0", "sass-embedded-linux-musl-arm64": "1.98.0", "sass-embedded-linux-musl-riscv64": "1.98.0", "sass-embedded-linux-musl-x64": "1.98.0", "sass-embedded-linux-riscv64": "1.98.0", "sass-embedded-linux-x64": "1.98.0", "sass-embedded-unknown-all": "1.98.0", "sass-embedded-win32-arm64": "1.98.0", "sass-embedded-win32-x64": "1.98.0" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg=="], + "sass-embedded": ["sass-embedded@1.99.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", "immutable": "^5.1.5", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-all-unknown": "1.99.0", "sass-embedded-android-arm": "1.99.0", "sass-embedded-android-arm64": "1.99.0", "sass-embedded-android-riscv64": "1.99.0", "sass-embedded-android-x64": "1.99.0", "sass-embedded-darwin-arm64": "1.99.0", "sass-embedded-darwin-x64": "1.99.0", "sass-embedded-linux-arm": "1.99.0", "sass-embedded-linux-arm64": "1.99.0", "sass-embedded-linux-musl-arm": "1.99.0", "sass-embedded-linux-musl-arm64": "1.99.0", "sass-embedded-linux-musl-riscv64": "1.99.0", "sass-embedded-linux-musl-x64": "1.99.0", "sass-embedded-linux-riscv64": "1.99.0", "sass-embedded-linux-x64": "1.99.0", "sass-embedded-unknown-all": "1.99.0", "sass-embedded-win32-arm64": "1.99.0", "sass-embedded-win32-x64": "1.99.0" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg=="], - "sass-embedded-all-unknown": ["sass-embedded-all-unknown@1.98.0", "", { "dependencies": { "sass": "1.98.0" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-6n4RyK7/1mhdfYvpP3CClS3fGoYqDvRmLClCESS6I7+SAzqjxvGG6u5Fo+cb1nrPNbbilgbM4QKdgcgWHO9NCA=="], + "sass-embedded-all-unknown": ["sass-embedded-all-unknown@1.99.0", "", { "dependencies": { "sass": "1.99.0" }, "cpu": [ "!arm", "!x64", "!arm64", ] }, "sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw=="], - "sass-embedded-android-arm": ["sass-embedded-android-arm@1.98.0", "", { "os": "android", "cpu": "arm" }, "sha512-LjGiMhHgu7VL1n7EJxTCre1x14bUsWd9d3dnkS2rku003IWOI/fxc7OXgaKagoVzok1kv09rzO3vFXJR5ZeONQ=="], + "sass-embedded-android-arm": ["sass-embedded-android-arm@1.99.0", "", { "os": "android", "cpu": "arm" }, "sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ=="], - "sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.98.0", "", { "os": "android", "cpu": "arm64" }, "sha512-M9Ra98A6vYJHpwhoC/5EuH1eOshQ9ZyNwC8XifUDSbRl/cGeQceT1NReR9wFj3L7s1pIbmes1vMmaY2np0uAKQ=="], + "sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.99.0", "", { "os": "android", "cpu": "arm64" }, "sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg=="], - "sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.98.0", "", { "os": "android", "cpu": "none" }, "sha512-WPe+0NbaJIZE1fq/RfCZANMeIgmy83x4f+SvFOG7LhUthHpZWcOcrPTsCKKmN3xMT3iw+4DXvqTYOCYGRL3hcQ=="], + "sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.99.0", "", { "os": "android", "cpu": "none" }, "sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw=="], - "sass-embedded-android-x64": ["sass-embedded-android-x64@1.98.0", "", { "os": "android", "cpu": "x64" }, "sha512-zrD25dT7OHPEgLWuPEByybnIfx4rnCtfge4clBgjZdZ3lF6E7qNLRBtSBmoFflh6Vg0RlEjJo5VlpnTMBM5MQQ=="], + "sass-embedded-android-x64": ["sass-embedded-android-x64@1.99.0", "", { "os": "android", "cpu": "x64" }, "sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ=="], - "sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.98.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cgr1z9rBnCdMf8K+JabIaYd9Rag2OJi5mjq08XJfbJGMZV/TA6hFJCLGkr5/+ZOn4/geTM5/3aSfQ8z5EIJAOg=="], + "sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.99.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg=="], - "sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.98.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-OLBOCs/NPeiMqTdOrMFbVHBQFj19GS3bSVSxIhcCq16ZyhouUkYJEZjxQgzv9SWA2q6Ki8GCqp4k6jMeUY9dcA=="], + "sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.99.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A=="], - "sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.98.0", "", { "os": "linux", "cpu": "arm" }, "sha512-03baQZCxVyEp8v1NWBRlzGYrmVT/LK7ZrHlF1piscGiGxwfdxoLXVuxsylx3qn/dD/4i/rh7Bzk7reK1br9jvQ=="], + "sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.99.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw=="], - "sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.98.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-axOE3t2MTBwCtkUCbrdM++Gj0gC0fdHJPrgzQ+q1WUmY9NoNMGqflBtk5mBZaWUeha2qYO3FawxCB8lctFwCtw=="], + "sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.99.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew=="], - "sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.98.0", "", { "os": "linux", "cpu": "arm" }, "sha512-OBkjTDPYR4hSaueOGIM6FDpl9nt/VZwbSRpbNu9/eEJcxE8G/vynRugW8KRZmCFjPy8j/jkGBvvS+k9iOqKV3g=="], + "sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.99.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ=="], - "sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.98.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-LeqNxQA8y4opjhe68CcFvMzCSrBuJqYVFbwElEj9bagHXQHTp9xVPJRn6VcrC+0VLEDq13HVXMv7RslIuU0zmA=="], + "sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.99.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw=="], - "sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.98.0", "", { "os": "linux", "cpu": "none" }, "sha512-7w6hSuOHKt8FZsmjRb3iGSxEzM87fO9+M8nt5JIQYMhHTj5C+JY/vcske0v715HCVj5e1xyTnbGXf8FcASeAIw=="], + "sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.99.0", "", { "os": "linux", "cpu": "none" }, "sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg=="], - "sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.98.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QikNyDEJOVqPmxyCFkci8ZdCwEssdItfjQFJB+D+Uy5HFqcS5Lv3d3GxWNX/h1dSb23RPyQdQc267ok5SbEyJw=="], + "sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.99.0", "", { "os": "linux", "cpu": "x64" }, "sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew=="], - "sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.98.0", "", { "os": "linux", "cpu": "none" }, "sha512-E7fNytc/v4xFBQKzgzBddV/jretA4ULAPO6XmtBiQu4zZBdBozuSxsQLe2+XXeb0X4S2GIl72V7IPABdqke/vA=="], + "sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.99.0", "", { "os": "linux", "cpu": "none" }, "sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA=="], - "sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.98.0", "", { "os": "linux", "cpu": "x64" }, "sha512-VsvP0t/uw00mMNPv3vwyYKUrFbqzxQHnRMO+bHdAMjvLw4NFf6mscpym9Bzf+NXwi1ZNKnB6DtXjmcpcvqFqYg=="], + "sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.99.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA=="], - "sass-embedded-unknown-all": ["sass-embedded-unknown-all@1.98.0", "", { "dependencies": { "sass": "1.98.0" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-C4MMzcAo3oEDQnW7L8SBgB9F2Fq5qHPnaYTZRMOH3Mp/7kM4OooBInXpCiiFjLnjY95hzP4KyctVx0uYR6MYlQ=="], + "sass-embedded-unknown-all": ["sass-embedded-unknown-all@1.99.0", "", { "dependencies": { "sass": "1.99.0" }, "os": [ "!linux", "!win32", "!darwin", "!android", ] }, "sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg=="], - "sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.98.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-nP/10xbAiPbhQkMr3zQfXE4TuOxPzWRQe1Hgbi90jv2R4TbzbqQTuZVOaJf7KOAN4L2Bo6XCTRjK5XkVnwZuwQ=="], + "sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.99.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw=="], - "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.98.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/lbrVsfbcbdZQ5SJCWcV0NVPd6YRs+FtAnfedp4WbCkO/ZO7Zt/58MvI4X2BVpRY/Nt5ZBo1/7v2gYcQ+J4svQ=="], + "sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.99.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg=="], - "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -3030,13 +3038,13 @@ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="], + "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], - "seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="], + "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], @@ -3056,7 +3064,7 @@ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], - "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], @@ -3072,7 +3080,7 @@ "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - "smol-toml": ["smol-toml@1.6.0", "", {}, "sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw=="], + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], @@ -3108,7 +3116,7 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "strnum": ["strnum@2.2.0", "", {}, "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg=="], + "strnum": ["strnum@2.2.3", "", {}, "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg=="], "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], @@ -3136,13 +3144,13 @@ "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], - "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], + "tailwindcss": ["tailwindcss@4.2.4", "", {}, "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA=="], "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.11", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ=="], + "tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], "tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="], @@ -3166,9 +3174,9 @@ "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], - "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], @@ -3202,16 +3210,12 @@ "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], - "type-fest": ["type-fest@5.5.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g=="], + "type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "ua-is-frozen": ["ua-is-frozen@0.1.2", "", {}, "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw=="], - - "ua-parser-js": ["ua-parser-js@2.0.9", "", { "dependencies": { "detect-europe-js": "^0.1.2", "is-standalone-pwa": "^0.1.1", "ua-is-frozen": "^0.1.2" }, "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w=="], - "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], @@ -3222,7 +3226,7 @@ "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], - "undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="], + "undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -3258,7 +3262,7 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - "unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="], + "unstorage": ["unstorage@1.17.5", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.10", "lru-cache": "^11.2.7", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -3286,11 +3290,11 @@ "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + "vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], "vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], - "vitefu": ["vitefu@1.1.2", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw=="], + "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], @@ -3314,11 +3318,11 @@ "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], - "workerd": ["workerd@1.20260312.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260312.1", "@cloudflare/workerd-darwin-arm64": "1.20260312.1", "@cloudflare/workerd-linux-64": "1.20260312.1", "@cloudflare/workerd-linux-arm64": "1.20260312.1", "@cloudflare/workerd-windows-64": "1.20260312.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-nNpPkw9jaqo79B+iBCOiksx+N62xC+ETIfyzofUEdY3cSOHJg6oNnVSHm7vHevzVblfV76c8Gr0cXHEapYMBEg=="], + "workerd": ["workerd@1.20260424.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260424.1", "@cloudflare/workerd-darwin-arm64": "1.20260424.1", "@cloudflare/workerd-linux-64": "1.20260424.1", "@cloudflare/workerd-linux-arm64": "1.20260424.1", "@cloudflare/workerd-windows-64": "1.20260424.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oKsB0Xo/mfkYMdSACoS06XZg09VUK4rXwHfF/1t3P++sMbwzf4UHQvMO57+zxpEB2nVrY/ZkW0bYFGq4GdAFSQ=="], "worktree-devservers": ["worktree-devservers@0.3.1", "", { "bin": { "dev-worktree": "dist/cli.js" } }, "sha512-DqmrscuBnqNEJR7H9GP19xO4c//yU7FsJmnsKsbCiBWEiBkdUspbCFt4fC8HVRIsfAbPcPH5X2fhygQYhnwUTw=="], - "wrangler": ["wrangler@4.73.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.15.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260312.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260312.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260312.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-VJXsqKDFCp6OtFEHXITSOR5kh95JOknwPY8m7RyQuWJQguSybJy43m4vhoCSt42prutTef7eeuw7L4V4xiynGw=="], + "wrangler": ["wrangler@4.85.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260424.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260424.1" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260424.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-93cwt2RPb1qdcmEgPzH7ybiLN4BIKoWpscIX6SywjHrQOeIZrQk2haoc3XMLKtQTmzapxza9OuDD+kMHpsuuhg=="], "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -3342,7 +3346,7 @@ "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -3364,21 +3368,21 @@ "zod-from-json-schema": ["zod-from-json-schema@0.5.2", "", { "dependencies": { "zod": "^4.0.17" } }, "sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g=="], - "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], - "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="], + "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + "@anthropic-ai/claude-agent-sdk/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="], - "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.22", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B2OTFcRw/Pdka9ZTjpXv6T6qZ6RruRuLokyb8HwW+aoW9ndJ3YasA3/mVswyJw7VMBF8ofXgqvcrCt9KYvFifg=="], + "@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@astrojs/react/@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], - "@astrojs/react/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "@astrojs/react/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], "@authenio/xml-encryption/xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="], @@ -3394,18 +3398,23 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@better-auth/api-key/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/core/better-call": ["better-call@1.1.4", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-NJouLY6IVKv0nDuFoc6FcbKDFzEnmgMNofC9F60Mwx1Ecm7X6/Ecyoe5b+JSVZ42F/0n46/M89gbYP1ZCVv8xQ=="], + "@better-auth/passkey/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/passkey/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], "@daveyplate/better-auth-ui/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], - "@decocms/runtime/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], - "@decocms/runtime/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@decocms/better-auth/better-call": ["better-call@1.1.5", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-nQJ3S87v6wApbDwbZ++FrQiSiVxWvZdjaO+2v6lZJAG2WWggkB2CziUDjPciz3eAt9TqfRursIQMZIcpkBnvlw=="], - "@grpc/proto-loader/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@freestyle-sh/with-deno/@freestyle-sh/with-type-js": ["@freestyle-sh/with-type-js@0.2.9", "", { "dependencies": { "@freestyle-sh/with-type-js-deps": "^0.2.9", "@freestyle-sh/with-type-run-code": "^0.2.9", "freestyle-sandboxes": "^0.1.28" } }, "sha512-W6rit2s71ekvD+0H+1rIzgovWHAa186mREJWyn4e+y4etW4+SrZ0Q+CF6a+EnkIWwnkPSL164SIbnJHUj3jp8w=="], + + "@grpc/proto-loader/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@inkjs/ui/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -3413,7 +3422,9 @@ "@instantdb/react/eventsource": ["eventsource@4.1.0", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-2GuF51iuHX6A9xdTccMTsNb7VO0lHZihApxhvQzJB5A03DvHDd2FQepodbMaztPBmBcE/ox7o2gqaxGhYB9LhQ=="], - "@kubernetes/client-node/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "@kubernetes/client-node/ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + + "@modelcontextprotocol/ext-apps/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], "@oclif/core/ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], @@ -3545,7 +3556,7 @@ "@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], - "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ=="], + "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.7.0", "", { "dependencies": { "@opentelemetry/core": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A=="], "@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], @@ -3565,7 +3576,7 @@ "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.6.0", "", { "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ=="], + "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@2.7.0", "", { "dependencies": { "@opentelemetry/core": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A=="], "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="], @@ -3651,13 +3662,13 @@ "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], @@ -3665,11 +3676,13 @@ "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "ai/@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "astro/vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "astro/vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], "astro/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -3679,6 +3692,10 @@ "better-auth/better-call": ["better-call@1.1.4", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-NJouLY6IVKv0nDuFoc6FcbKDFzEnmgMNofC9F60Mwx1Ecm7X6/Ecyoe5b+JSVZ42F/0n46/M89gbYP1ZCVv8xQ=="], + "better-call/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + + "better-call/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + "boxen/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -3703,15 +3720,13 @@ "debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "decocms/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.3.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RkOUMSetrbS1i8kW1wIkfuq0RpXtiJOiFCx/AfEjGNZA8xOjdAosqPiImo2805Q6Px/9k1LUxu8NUmlSnrWrqg=="], - - "decocms/@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], + "decocms/@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.8.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-oDDW/0KMqz4suHVloB9sNv0YyKLGNYf1FTevXH6adDkid5dsmbbcYuiEsbIhpZSZtHa6o5AVjK1jEAfePOLxww=="], "decocms/date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], "decocms/lucide-react": ["lucide-react@0.468.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA=="], - "decocms/recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="], + "decocms/recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="], "decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], @@ -3723,13 +3738,15 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "freestyle/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + "freestyle-sandboxes/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], "git-diff/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], "git-diff/diff": ["diff@3.5.1", "", {}, "sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw=="], - "h3/cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], + "h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], "hast-util-from-parse5/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], @@ -3745,12 +3762,8 @@ "ink/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - "ink/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "kysely-codegen/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "kysely-codegen/kysely": ["kysely@0.27.6", "", {}, "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ=="], - "kysely-pglite/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "kysely-pglite/jiti": ["jiti@2.0.0-beta.3", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-pmfRbVRs/7khFrSAYnSiJ8C0D5GvzkE4Ey2pAvUcJsw1ly/p+7ut27jbJrjY79BpAJQJ4gXYFtK6d1Aub+9baQ=="], @@ -3759,18 +3772,10 @@ "mdast-util-mdx-jsx/parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], - "mesh-plugin-user-sandbox/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], - "mesh-plugin-user-sandbox/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], - - "mesh-plugin-workflows/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], - "mesh-plugin-workflows/@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], - - "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], - "openid-client/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], - "p-queue/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "parse-entities/character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="], @@ -3785,9 +3790,7 @@ "refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="], - "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - - "samlify/camelcase": ["camelcase@9.0.0", "", {}, "sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw=="], + "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -3827,8 +3830,17 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "@decocms/runtime/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - "@decocms/runtime/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "@better-auth/core/better-call/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + + "@better-auth/core/better-call/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "@decocms/better-auth/better-call/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + + "@decocms/better-auth/better-call/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@decocms/better-auth/better-call/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], "@oclif/core/ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], @@ -3852,7 +3864,7 @@ "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/sdk-logs/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], @@ -3864,7 +3876,7 @@ "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], @@ -3874,7 +3886,7 @@ "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], @@ -3882,7 +3894,7 @@ "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], @@ -3890,7 +3902,7 @@ "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], @@ -3898,7 +3910,7 @@ "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], @@ -3906,7 +3918,7 @@ "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], @@ -3914,7 +3926,7 @@ "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="], - "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.207.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="], @@ -3926,7 +3938,7 @@ "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="], - "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.207.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="], @@ -3942,6 +3954,12 @@ "astro/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "better-auth/better-call/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + + "better-auth/better-call/@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "better-auth/better-call/set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -3952,13 +3970,11 @@ "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "decocms/@types/bun/bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], - "decocms/recharts/eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], "decocms/recharts/victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], @@ -3968,6 +3984,12 @@ "freestyle-sandboxes/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + "freestyle/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + + "freestyle/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "freestyle/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + "git-diff/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "git-diff/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], @@ -3990,12 +4012,6 @@ "mdast-util-mdx-jsx/parse-entities/is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "mesh-plugin-user-sandbox/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - "mesh-plugin-user-sandbox/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - - "mesh-plugin-workflows/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - "mesh-plugin-workflows/@types/bun/bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], - "shelljs/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], @@ -4068,7 +4084,7 @@ "@oclif/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer/protobufjs": ["protobufjs@7.5.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg=="], "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -4130,11 +4146,11 @@ "git-diff/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - "kysely-pglite/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "kysely-pglite/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "mdast-util-mdx-jsx/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], - "shelljs/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "shelljs/glob/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], From 4c63497338c5ace74f28c5d99360c8f40aa33595 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Fri, 24 Apr 2026 22:21:02 -0300 Subject: [PATCH 03/16] feat(sandbox): background bootstrap with live SSE log streaming VM_START used to block until clone+install finished (~30s on medium repos). The Terminal tab opens its SSE connection only after VM_START returns, so users saw the entire setup output dumped at once via `replayTo` instead of streaming live. Run repo bootstrap in the background (Docker + K8s runners) and persist the handle BEFORE bootstrap so /api/sandbox/ resolves while clone is still running. Bootstrap output streams through the daemon's log ring under a `setup` source so the Terminal tab can subscribe via SSE. Daemon side: split bash output on any CR/LF run so git's progress reports surface as distinct log lines instead of accumulating until the trailing newline. Set CI=1 in dev-process env so Vite's interactive shortcut reader doesn't EOF and exit when stdio is ignored. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/mesh-plugin-user-sandbox/README.md | 46 ++++++- .../mesh-plugin-user-sandbox/image/daemon.mjs | 15 ++- .../image/daemon/dev-process.mjs | 5 + .../image/daemon/events.mjs | 5 +- .../server/daemon-client.ts | 15 ++- .../server/runner/docker/runner.test.ts | 41 ++++--- .../server/runner/docker/runner.ts | 114 +++++++++++++----- .../server/runner/k8s/runner.ts | 85 +++++++++++-- 8 files changed, 258 insertions(+), 68 deletions(-) diff --git a/packages/mesh-plugin-user-sandbox/README.md b/packages/mesh-plugin-user-sandbox/README.md index 8d831f7ce8..934940878f 100644 --- a/packages/mesh-plugin-user-sandbox/README.md +++ b/packages/mesh-plugin-user-sandbox/README.md @@ -2,6 +2,42 @@ Isolated per-user sandboxes for MCP tool execution. +One sandbox per `(userId, projectRef)`: a container (or VM) holding a checked-out +repo plus an in-pod daemon that proxies exec, file ops, and the dev server. +Callers go through a single `SandboxRunner` interface; the runner decides how +the sandbox is provisioned and reached. + +## Runners + +Three runner backends live behind the common `SandboxRunner` interface +(`server/runner/types.ts`): + +- **Docker** (`./runner`) — default for local dev. Spawns containers via the + local Docker CLI and routes browser traffic through an in-process ingress + bound on `SANDBOX_INGRESS_PORT`. +- **Freestyle** (`./runner/freestyle`) — hosted VMs. Preview URL is a + Freestyle-provided HTTPS domain; daemon traffic is base64-wrapped to clear + Cloudflare WAF. SDKs are `optionalDependencies` and only pulled in when this + runner is selected. +- **Kubernetes** (`./runner/k8s`) — one `SandboxClaim` per sandbox against the + [kubernetes-sigs/agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) + operator. Mesh talks to pods via apiserver port-forward in dev; in prod, + `previewUrlPattern` switches the preview URL to real ingress and skips the + dev forward. + +### Selection + +The host app calls `resolveRunnerKindFromEnv()` / `tryResolveRunnerKindFromEnv()` +from `./runner`: + +1. `MESH_SANDBOX_RUNNER=docker|freestyle|kubernetes` wins when set. +2. Otherwise, `FREESTYLE_API_KEY` present → `freestyle`. +3. Otherwise, in `NODE_ENV=production` → unresolved (strict variant throws). +4. Otherwise (dev) → `docker` if the CLI is on `PATH`, else unresolved. + +Kubernetes is **explicit-only** — it's never auto-selected, so docker-only +deploys don't accidentally need a kubeconfig. + ## URL shape - **Prod**: `https://.sandboxes./*` → pod dev server on `:3000` @@ -12,7 +48,7 @@ Handles are a 128-bit hex prefix of the container id — 32 chars, DNS-label safe (RFC 1035 caps labels at 63), still cryptographically secret as a capability. The URL itself is the capability. -## Local dev +## Local dev (Docker) The local ingress forwarder binds both `127.0.0.1` and `::1` on `SANDBOX_INGRESS_PORT` (default `7070`) and routes requests by `Host:` header. @@ -27,7 +63,13 @@ for this, you can remove them — they're no longer needed. ## Environment -- `SANDBOX_INGRESS_PORT` (default `7070`) — local forwarder bind port. +- `MESH_SANDBOX_RUNNER` — pin the runner: `docker`, `freestyle`, or + `kubernetes`. Leave unset in dev to let auto-detect pick docker. +- `FREESTYLE_API_KEY` — required for the Freestyle runner. Presence also + auto-selects it when `MESH_SANDBOX_RUNNER` is unset. +- `MESH_SANDBOX_IMAGE` — override the Docker runner image + (default `mesh-sandbox:local`, built from `image/Dockerfile`). +- `SANDBOX_INGRESS_PORT` (default `7070`) — local Docker ingress bind port. - `SANDBOX_ROOT_URL` — production template for the pod URL. Either a bare base (`https://sandboxes.example.com` → handle becomes leading subdomain) or a `{handle}` template (`https://{handle}.sandboxes.example.com`). diff --git a/packages/mesh-plugin-user-sandbox/image/daemon.mjs b/packages/mesh-plugin-user-sandbox/image/daemon.mjs index 05fa54cbe0..4e6b762b26 100644 --- a/packages/mesh-plugin-user-sandbox/image/daemon.mjs +++ b/packages/mesh-plugin-user-sandbox/image/daemon.mjs @@ -50,7 +50,7 @@ import { inspectWorkdir } from "./daemon/workdir.mjs"; const DAEMON_PREFIX = "/_daemon"; -function runBash(command, timeoutMs, cwd = WORKDIR) { +function runBash(command, timeoutMs, cwd = WORKDIR, logSource = null) { return new Promise((resolve) => { const child = spawn("bash", ["-lc", command], { cwd, env: childEnv() }); let stdout = ""; @@ -62,9 +62,11 @@ function runBash(command, timeoutMs, cwd = WORKDIR) { }, timeoutMs); child.stdout.on("data", (d) => { stdout += d.toString(); + if (logSource) appendLog(logSource, d); }); child.stderr.on("data", (d) => { stderr += d.toString(); + if (logSource) appendLog(logSource, d); }); child.on("close", (code) => { clearTimeout(timer); @@ -281,12 +283,19 @@ const server = http.createServer(async (req, res) => { if (req.method === "POST" && subUrl === "/bash") { try { const body = await parsedBody(req); - const { command, timeout = 60_000, cwd } = body; + const { command, timeout = 60_000, cwd, logSource } = body; if (typeof command !== "string" || command.length === 0) { send(res, 400, { error: "command is required" }); return; } - const result = await runBash(command, Number(timeout), cwd); + const result = await runBash( + command, + Number(timeout), + cwd, + typeof logSource === "string" && logSource.length > 0 + ? logSource + : null, + ); send(res, 200, result); } catch (err) { send(res, 500, { error: String(err) }); diff --git a/packages/mesh-plugin-user-sandbox/image/daemon/dev-process.mjs b/packages/mesh-plugin-user-sandbox/image/daemon/dev-process.mjs index a167332c49..c503cad2de 100644 --- a/packages/mesh-plugin-user-sandbox/image/daemon/dev-process.mjs +++ b/packages/mesh-plugin-user-sandbox/image/daemon/dev-process.mjs @@ -202,6 +202,11 @@ export async function startDev({ const env = childEnv({ HOST: "0.0.0.0", PORT: String(DEV_PORT), + // Vite's interactive shortcut reader (press h/b/q + enter) blocks on + // stdin; with stdio:"ignore" below, it sees EOF and exits 0 right after + // printing its banner. CI=1 disables the reader so the dev server keeps + // running. + CI: "1", }); // `script -q -c` gives the child a PTY so ANSI colors / progress output diff --git a/packages/mesh-plugin-user-sandbox/image/daemon/events.mjs b/packages/mesh-plugin-user-sandbox/image/daemon/events.mjs index 084280e2cf..9eb08cb341 100644 --- a/packages/mesh-plugin-user-sandbox/image/daemon/events.mjs +++ b/packages/mesh-plugin-user-sandbox/image/daemon/events.mjs @@ -9,7 +9,10 @@ const backpressureCounts = new WeakMap(); export function appendLog(source, chunk) { const text = typeof chunk === "string" ? chunk : chunk.toString("utf8"); - const lines = text.split(/\r?\n/); + // Split on any run of CR/LF so bare `\r` (git-clone progress "Receiving + // objects: X%\r...") surfaces as distinct lines instead of accumulating + // until the trailing newline git writes when done. + const lines = text.split(/[\r\n]+/); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.length === 0 && i === lines.length - 1) continue; diff --git a/packages/mesh-plugin-user-sandbox/server/daemon-client.ts b/packages/mesh-plugin-user-sandbox/server/daemon-client.ts index f4314c81dc..221a21db63 100644 --- a/packages/mesh-plugin-user-sandbox/server/daemon-client.ts +++ b/packages/mesh-plugin-user-sandbox/server/daemon-client.ts @@ -62,6 +62,7 @@ export async function daemonBash( daemonUrl: string, token: string, input: ExecInput, + opts?: { logSource?: string }, ): Promise { const timeoutMs = input.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS; const response = await fetch(`${daemonUrl}/_daemon/bash`, { @@ -75,6 +76,7 @@ export async function daemonBash( timeout: timeoutMs, cwd: input.cwd, env: input.env, + logSource: opts?.logSource, }), signal: AbortSignal.timeout(timeoutMs + 5_000), }); @@ -137,10 +139,15 @@ export async function bootstrapRepo( .filter((part): part is string => part !== null) .join(" && "); // Medium repos routinely exceed the default 60s exec timeout. - const result = await daemonBash(daemonUrl, token, { - command: cmd, - timeoutMs: 10 * 60_000, - }); + // `logSource: "setup"` streams git clone/checkout output through the daemon's + // log ring so the Terminal tab's Setup pane can replay it on SSE connect — + // matches the freestyle flow where setup logs come from the systemd unit. + const result = await daemonBash( + daemonUrl, + token, + { command: cmd, timeoutMs: 10 * 60_000 }, + { logSource: "setup" }, + ); if (result.exitCode !== 0) { throw new Error( `sandbox repo bootstrap failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`, diff --git a/packages/mesh-plugin-user-sandbox/server/runner/docker/runner.test.ts b/packages/mesh-plugin-user-sandbox/server/runner/docker/runner.test.ts index eee7ee0f28..97e5e47e34 100644 --- a/packages/mesh-plugin-user-sandbox/server/runner/docker/runner.test.ts +++ b/packages/mesh-plugin-user-sandbox/server/runner/docker/runner.test.ts @@ -326,7 +326,7 @@ describe("DockerSandboxRunner.ensure() — resume from persisted state", () => { }); describe("DockerSandboxRunner.ensure() — attach-repo failure cleanup", () => { - it("stops the orphaned container and rethrows when bootstrap fails", async () => { + it("stops the orphaned container when background bootstrap fails", async () => { const stopCalls: string[] = []; const { exec } = makeExec((args) => { if (args[0] === "stop") { @@ -347,24 +347,33 @@ describe("DockerSandboxRunner.ensure() — attach-repo failure cleanup", () => { const runner = new DockerSandboxRunner({ exec, stateStore: store }); const handle = FAKE_ID.slice(0, 32); - await expect( - runner.ensure(ID, { - repo: { - cloneUrl: "https://example.com/r.git", - userName: "u", - userEmail: "u@e", - }, - }), - ).rejects.toThrow(/bootstrap boom/); - + // Bootstrap is now async — ensure() resolves as soon as the daemon is + // healthy, and the clone runs in the background so setup logs stream to + // the Terminal tab live. + const sandbox = await runner.ensure(ID, { + repo: { + cloneUrl: "https://example.com/r.git", + userName: "u", + userEmail: "u@e", + }, + }); + expect(sandbox.handle).toBe(handle); + + // Poll for the background teardown — chained microtasks across a mock + // fetch and a stopContainer call should settle within a few ticks. + const deadline = Date.now() + 500; + while (!stopCalls.includes(handle) && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 5)); + } expect(stopCalls).toContain(handle); - // byHandle entry removed → alive uses exec_('inspect') which would still - // return "true" for our mock; instead verify via lookup path by calling - // `resolveDevPort` which reads byHandle first, then store. Since persist - // runs AFTER bootstrap, the store is also empty. + + // The pre-bootstrap persist (required so /api/sandbox/:handle resolves + // while clone runs) is cleaned up in the failure path, so the handle + // no longer resolves afterwards. + expect(store.putCalls).toHaveLength(1); + expect(store.deleteByHandleCalls.map((c) => c.handle)).toContain(handle); const devPort = await runner.resolveDevPort(handle); expect(devPort).toBeNull(); - expect(store.putCalls).toHaveLength(0); }); }); diff --git a/packages/mesh-plugin-user-sandbox/server/runner/docker/runner.ts b/packages/mesh-plugin-user-sandbox/server/runner/docker/runner.ts index de7d2b0e9c..4293d2afde 100644 --- a/packages/mesh-plugin-user-sandbox/server/runner/docker/runner.ts +++ b/packages/mesh-plugin-user-sandbox/server/runner/docker/runner.ts @@ -75,6 +75,12 @@ interface DockerRecord { devPort: number; devContainerPort: number; repoAttached: boolean; + /** + * In-flight repo bootstrap, or null when idle. See the K8s runner — we run + * clone/checkout in the background so the Terminal tab catches setup logs + * via SSE instead of seeing the full backlog dumped at once via `replayTo`. + */ + bootstrapPromise: Promise | null; workload: Workload | null; } @@ -247,40 +253,85 @@ export class DockerSandboxRunner implements SandboxRunner { persistNow: boolean, ): Promise { this.records.set(rec.handle, rec); - let bootstrapped = false; - if (opts.repo && !rec.repoAttached) { - try { - await bootstrapRepo(rec.daemonUrl, rec.token, rec.workdir, opts.repo); - rec.repoAttached = true; - bootstrapped = true; - } catch (err) { - this.records.delete(rec.handle); - await this.stopContainer(rec.handle).catch((teardownErr) => - console.warn( - `[${LOG_LABEL}] orphan teardown after attach failure handle=${rec.handle} attachErr="${ - err instanceof Error ? err.message : String(err) - }" teardownErr="${ - teardownErr instanceof Error - ? teardownErr.message - : String(teardownErr) - }"`, - ), - ); - throw err; - } + // Persist BEFORE starting background bootstrap so the handle is resolvable + // via /api/sandbox/:handle/_daemon/... as soon as VM_START returns (see + // K8s runner `finish` for the full rationale). + if (persistNow) await this.persist(ops, rec); + // Bootstrap in the background — see K8s runner `finish` for the rationale + // (Terminal tab subscribes only after VM_START returns; if we `await` the + // clone here, the setup logs arrive as a post-hoc burst via `replayTo`). + if (opts.repo && !rec.repoAttached && !rec.bootstrapPromise) { + rec.bootstrapPromise = this.bootstrapAndStart(rec, opts); + } else if (!opts.repo || rec.repoAttached) { + startDevServer( + rec.daemonUrl, + rec.token, + opts.workload ?? rec.workload, + LOG_LABEL, + ); } - // Persist on fresh provision/adopt, OR after we just flipped repoAttached - // on a resumed record (so subsequent ensure() skips bootstrap). - if (persistNow || bootstrapped) await this.persist(ops, rec); - startDevServer( - rec.daemonUrl, - rec.token, - opts.workload ?? rec.workload, - LOG_LABEL, - ); return this.toSandbox(rec); } + /** + * Uses `this.stateStore` (unscoped) for the post-bootstrap persist — the + * `ops` view from the caller's `withLock` scope is bound to a transaction + * that closed when VM_START returned. + */ + private async bootstrapAndStart( + rec: DockerRecord, + opts: EnsureOptions, + ): Promise { + try { + await bootstrapRepo(rec.daemonUrl, rec.token, rec.workdir, opts.repo!); + rec.repoAttached = true; + await this.persist(this.stateStore, rec).catch((err) => + console.warn( + `[${LOG_LABEL}] persist after bootstrap failed for ${rec.handle}: ${err instanceof Error ? err.message : String(err)}`, + ), + ); + startDevServer( + rec.daemonUrl, + rec.token, + opts.workload ?? rec.workload, + LOG_LABEL, + ); + } catch (err) { + // Bootstrap failure used to tear the container down synchronously so a + // failed VM_START wouldn't leak a half-broken sandbox. With the async + // flow VM_START has already returned 200, so the user can't see the + // error on the call — tear down anyway to match the old contract and + // avoid stranding a container that can't serve the project. + console.warn( + `[${LOG_LABEL}] background bootstrap failed for ${rec.handle}: ${err instanceof Error ? err.message : String(err)}`, + ); + this.records.delete(rec.handle); + // We persisted synchronously before kicking off bootstrap (so the proxy + // at /api/sandbox/:handle could resolve immediately). Clean it up now + // so the next VM_START doesn't rehydrate an orphan row. + if (this.stateStore) { + await this.stateStore + .deleteByHandle(RUNNER_KIND, rec.handle) + .catch((err) => + console.warn( + `[${LOG_LABEL}] state cleanup after attach failure handle=${rec.handle} err="${err instanceof Error ? err.message : String(err)}"`, + ), + ); + } + await this.stopContainer(rec.handle).catch((teardownErr) => + console.warn( + `[${LOG_LABEL}] orphan teardown after attach failure handle=${rec.handle} teardownErr="${ + teardownErr instanceof Error + ? teardownErr.message + : String(teardownErr) + }"`, + ), + ); + } finally { + rec.bootstrapPromise = null; + } + } + private async provision( id: SandboxId, labelId: string, @@ -360,6 +411,7 @@ export class DockerSandboxRunner implements SandboxRunner { devPort, devContainerPort, repoAttached: false, + bootstrapPromise: null, workload: opts.workload ?? null, }; } @@ -397,6 +449,7 @@ export class DockerSandboxRunner implements SandboxRunner { devPort, devContainerPort, repoAttached: state.repoAttached ?? false, + bootstrapPromise: null, workload: state.workload ?? null, }; } @@ -458,6 +511,7 @@ export class DockerSandboxRunner implements SandboxRunner { devPort, devContainerPort, repoAttached: false, + bootstrapPromise: null, workload: opts.workload ?? null, }; } diff --git a/packages/mesh-plugin-user-sandbox/server/runner/k8s/runner.ts b/packages/mesh-plugin-user-sandbox/server/runner/k8s/runner.ts index f7a3b4a061..808affd2cd 100644 --- a/packages/mesh-plugin-user-sandbox/server/runner/k8s/runner.ts +++ b/packages/mesh-plugin-user-sandbox/server/runner/k8s/runner.ts @@ -121,6 +121,14 @@ interface K8sRecord { devForward: PortForwarder | null; repoAttached: boolean; workload: Workload | null; + /** + * In-flight repo bootstrap, or null when idle. Set when VM_START kicks off + * clone/checkout in the background so the browser opens SSE against the + * daemon while git is still running and watches setup logs stream live. + * Concurrent VM_STARTs for the same handle join this promise instead of + * racing a second `git clone`. + */ + bootstrapPromise: Promise | null; /** * Per-daemon-boot UUID. `/app` lives in the container's ephemeral layer * (see sandbox-template.yaml — no volumeMount), so any OOMKill/crash @@ -347,19 +355,31 @@ export class KubernetesSandboxRunner implements SandboxRunner { patchTtl: boolean, ): Promise { this.records.set(rec.handle, rec); - let bootstrapped = false; - if (opts.repo && !rec.repoAttached) { - await bootstrapRepo(rec.daemonUrl, rec.token, rec.workdir, opts.repo); - rec.repoAttached = true; - bootstrapped = true; + // Persist BEFORE starting background bootstrap. The browser opens SSE at + // `/api/sandbox//_daemon/...` as soon as VM_START returns, and + // that route authorizes by looking up `sandbox_runner_state` by handle + // (apps/mesh/src/api/routes/sandbox-daemon.ts) — if the row isn't there + // yet, the proxy 404s and the Terminal tab flips to "VM suspended" while + // clone runs. Persisting with `repoAttached: false` makes the handle + // resolvable immediately; bootstrapAndStart re-persists on success. + if (persistNow) await this.persist(ops, rec); + // Return VM_START as soon as the daemon is reachable. The clone/checkout + // takes ~30s on medium repos, and the browser only opens SSE after it + // learns `daemonUrl` from this call's result — blocking here means the + // Terminal tab subscribes *after* setup already ran, so it sees the whole + // log backlog as a single burst via `replayTo`. Running bootstrap in the + // background lets the browser watch git progress stream live. + if (opts.repo && !rec.repoAttached && !rec.bootstrapPromise) { + rec.bootstrapPromise = this.bootstrapAndStart(rec, opts); + } else if (!opts.repo || rec.repoAttached) { + // No bootstrap needed — start the dev server right away. + startDevServer( + rec.daemonUrl, + rec.token, + opts.workload ?? rec.workload, + LOG_LABEL, + ); } - if (persistNow || bootstrapped) await this.persist(ops, rec); - startDevServer( - rec.daemonUrl, - rec.token, - opts.workload ?? rec.workload, - LOG_LABEL, - ); // Fresh provision set a shutdownTime in the claim spec already; resumes // and adopts rely on this patch to stay alive. if (patchTtl) { @@ -377,6 +397,44 @@ export class KubernetesSandboxRunner implements SandboxRunner { return this.toSandbox(rec); } + /** + * Runs clone/checkout against the daemon, then kicks the dev server. Logs + * stream through the daemon's SSE ring (see daemon.mjs `runBash`'s + * `logSource` path) so the Terminal tab shows them live. On failure we leave + * the sandbox alive — user retries by clicking Run again, which enters + * `finish` with `repoAttached: false` and the promise cleared. + * + * Uses `this.stateStore` (unscoped) for the post-bootstrap persist — NOT + * the `ops` view from the original `withLock` scope, which is bound to a + * transaction that closed when VM_START returned. + */ + private async bootstrapAndStart( + rec: K8sRecord, + opts: EnsureOptions, + ): Promise { + try { + await bootstrapRepo(rec.daemonUrl, rec.token, rec.workdir, opts.repo!); + rec.repoAttached = true; + await this.persist(this.stateStore, rec).catch((err) => + console.warn( + `[${LOG_LABEL}] persist after bootstrap failed for ${rec.handle}: ${err instanceof Error ? err.message : String(err)}`, + ), + ); + startDevServer( + rec.daemonUrl, + rec.token, + opts.workload ?? rec.workload, + LOG_LABEL, + ); + } catch (err) { + console.warn( + `[${LOG_LABEL}] background bootstrap failed for ${rec.handle}: ${err instanceof Error ? err.message : String(err)}`, + ); + } finally { + rec.bootstrapPromise = null; + } + } + private async provision( id: SandboxId, handle: string, @@ -454,6 +512,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { daemonForward, devForward, repoAttached: false, + bootstrapPromise: null, workload: opts.workload ?? null, daemonBootId: await readDaemonBootId(daemonUrl), }; @@ -536,6 +595,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { daemonForward, devForward, repoAttached: rebooted ? false : (state.repoAttached ?? false), + bootstrapPromise: null, workload: state.workload ?? null, daemonBootId: currentBootId, }; @@ -580,6 +640,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { devForward, // Adopted: repo state unknown → next ensure re-runs attach if needed. repoAttached: false, + bootstrapPromise: null, workload: null, daemonBootId: await readDaemonBootId(daemonUrl), }; From c49a352803ef541f4f1caa28f5fafa884bf5b0b7 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Mon, 27 Apr 2026 21:47:26 -0300 Subject: [PATCH 04/16] rm --- .../PLAN-GITHUB-SYNC.md | 354 ------------- .../mesh-plugin-user-sandbox/PLAN-K8S-MVP.md | 421 --------------- .../PLAN-K8S-REMAINING.md | 486 ------------------ 3 files changed, 1261 deletions(-) delete mode 100644 packages/mesh-plugin-user-sandbox/PLAN-GITHUB-SYNC.md delete mode 100644 packages/mesh-plugin-user-sandbox/PLAN-K8S-MVP.md delete mode 100644 packages/mesh-plugin-user-sandbox/PLAN-K8S-REMAINING.md diff --git a/packages/mesh-plugin-user-sandbox/PLAN-GITHUB-SYNC.md b/packages/mesh-plugin-user-sandbox/PLAN-GITHUB-SYNC.md deleted file mode 100644 index c11e9f5b70..0000000000 --- a/packages/mesh-plugin-user-sandbox/PLAN-GITHUB-SYNC.md +++ /dev/null @@ -1,354 +0,0 @@ -# GitHub sync for thread work - -## Goal - -Make the per-thread branch the durable artifact. A user working in a thread -can hit "Publish" and have their work appear on GitHub as a branch (and -optionally a PR) attributed to them — without tokens landing on disk in the -pod, and without the first user's identity getting stamped on everyone -else's commits in a shared `(org, agent)` container. - -## Non-goals - -- No auto-push. Pushes are explicit user actions. A separate plan can - revisit auto-publish if we want it later; the cost is surprise (and - ugly WIP commits in GitHub) so it's not a default. -- No PR creation from inside the sandbox. PR creation needs org-level - GitHub app context that doesn't belong in a user-code container. - Mesh opens the PR, the daemon just handles `git push`. -- No GitHub App install flow. Assume the org already has an install and - a token resolver exists in mesh. If it doesn't, that's a prereq, not - this plan. -- No cross-remote sync (GitLab, Bitbucket). GitHub only for now. The - design generalizes, but provider-specific quirks (PR API, auth flow) - are deferred until there's demand. - -## Invariants - -1. Per-exec identity is the source of truth for who committed. No - pod-global git config. A commit's author is whoever fired the tool - call, resolved at `/bash` time, not whoever happened to start the - pod. -2. Repo push tokens never touch disk in the pod. Not in `.git/config`, - not in `~/.git-credentials`, not in env that outlives the exec. - `GIT_ASKPASS` callback + short-lived tokens. -3. One thread → one branch. Pushes are idempotent re-publishes of that - branch. `--force-with-lease` is acceptable; `--force` is not. -4. AI-assisted dev framing. This is not a prod deploy target. We don't - need immutable commit-pinned containers, attestation, or supply-chain - guarantees here. - -## Identity: per-exec, not pod-global - -Today: `bootstrapRepo` in `server/runner/docker.ts:885` runs -`gitIdentityScript(repo.userName, repo.userEmail)`, which does -`git config --global user.name/email`. In a `(org, agent)`-scoped pod, -the first user's identity stamps every subsequent user's commits. Data -integrity problem. - -Fix: - -- Delete the `gitIdentityScript` call from `bootstrapRepo`. -- Keep the `gitIdentityScript` export in `shared.ts` only if something - else uses it; otherwise delete. -- Inject identity per `/bash` call via env: - - ``` - GIT_AUTHOR_NAME= - GIT_AUTHOR_EMAIL= - GIT_COMMITTER_NAME= - GIT_COMMITTER_EMAIL= - ``` - - Git honors these without any config. The acting user is whoever's - session fired the tool call, not `thread.created_by` — if Bob commits - in Alice's thread, Bob is the author. - -- Verify the daemon's `/bash` handler accepts `env` and passes it to - the spawned child. If it doesn't, wire it. The endpoint signature - already takes `{ command, timeoutMs, cwd }`; adding `env` is a - one-line change to the spawn call. - -- Runner's `exec()` contract grows an optional `env` field. Docker - runner forwards it to the daemon. Freestyle runner already has its - own env handling; plumb the same field through. - -## Auth: ASKPASS callback - -Initial clone keeps its current behavior (a short-lived token in the -clone URL, passed as env so it doesn't land in shell history). But -after clone: - -- Rewrite `origin` to the tokenless URL so nothing sticks in - `.git/config`. One line in `bootstrapRepo` after the clone succeeds: - `git remote set-url origin `. -- Bake a `GIT_ASKPASS` helper script into the image. Tiny shell or - node script: reads an exec token from env, POSTs to the daemon's - credential endpoint, prints the token to stdout, exits. Git calls - it for password prompts. -- Add `POST /credentials` (daemon) — takes the exec token, proxies to - mesh (`POST /api/sandbox/credentials` or similar), mesh resolves the - acting user's GitHub token from the vault, returns it. Short TTL on - the exec token (10 min). -- Per-exec env for push calls: - - ``` - GIT_ASKPASS=/usr/local/bin/askpass - DECO_EXEC_TOKEN= - ``` - - The token is minted by mesh at tool-call time, scoped to the acting - user + sandbox handle. Never reused. - -Resolving the user's GitHub token on the mesh side is a prereq. Needs -confirmation that the vault has a per-user token (from a user-level -GitHub OAuth grant) or that we can scope an org-level app install -token to the acting user. See open questions. - -## Branch naming - -`thread/` is functional but ugly in the GitHub PR list. Before -push lands, switch to a human-friendly scheme: - -- Default: `deco//`. -- Fallback when title is missing/empty: `deco/thread/` (first - 8 chars of the thread id). -- Slug sanitization: `[^a-z0-9-]` → `-`, collapse runs, trim to 60 chars. -- Collision handling: if the target branch already exists on remote - with unrelated history, append `-`. Rare in practice (title - collisions within one agent). - -Applies at worktree creation (`ensureThreadWorkspace` in -`server/ensure-worktree.ts:68`). Existing threads with `thread/` -branches keep them — naming is sticky once the worktree exists. -Rename-on-first-push is possible but not worth the edge cases; leave -legacy threads alone. - -## Push flow (happy path) - -1. User clicks "Publish" in the thread UI. -2. UI calls mesh tool `THREAD_PUBLISH` (name TBD) with `{ threadId, - message?, openPr? }`. -3. Mesh resolves acting user → mints exec token → resolves thread's - worktree cwd (via `ensureThreadWorkspace`) → calls daemon `/bash` - with per-exec env (`GIT_AUTHOR_*`, `GIT_COMMITTER_*`, `GIT_ASKPASS`, - `DECO_EXEC_TOKEN`): - - ``` - set -e - cd - if [ -n "$(git status --porcelain)" ]; then - git add -A - git commit -m "'>" - fi - git push --force-with-lease -u origin - ``` - -4. If `openPr` is true, mesh (not daemon) calls the GitHub API to open - a PR from `` to the repo's default branch. Returns the - PR URL. -5. UI shows the branch URL (and PR URL if opened). - -Failure modes to handle explicitly: - -- Dirty index with no real changes (whitespace-only, etc): commit - anyway if `git add -A` staged something; skip push if `git diff - origin/` is empty after the commit. -- Push rejected (diverged remote): surface `git push` stderr verbatim - so the user can act. `--force-with-lease` guards against clobbering - unseen commits; don't swallow the error. -- No GitHub token for the acting user: fail fast with a structured - `GITHUB_AUTH_MISSING` error; UI prompts the user to connect their - GitHub account. -- Push auth failed (revoked token, expired): same — structured error, - UI reconnect flow. - -## Daemon surface - -New endpoints (all bearer-authed via the existing `TOKEN`): - -- `POST /credentials` — ASKPASS callback. Body: `{ execToken, remote? }`. - Returns `{ token }` on success. Called only by the baked-in askpass - helper, not by mesh directly. Proxies to mesh's credential endpoint. - Logs the request (not the token) for audit. - -Env additions on `/bash`: - -- `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL` -- `GIT_COMMITTER_NAME`, `GIT_COMMITTER_EMAIL` -- `GIT_ASKPASS` (set when pushing) -- `DECO_EXEC_TOKEN` (set when pushing) - -No new endpoint for "publish" — the mesh tool composes the existing -`/bash` endpoint. Keeps the daemon ignorant of GitHub. - -## Mesh surface - -New tool: - -``` -THREAD_PUBLISH - input: - threadId: string - message?: string // commit message; defaults to thread title - openPr?: boolean // default false - prTitle?: string - prBody?: string - output: - branch: string - branchUrl: string // https://github.com///tree/ - commitSha: string | null // null if nothing to commit - prUrl?: string -``` - -Authorization: `ctx.access.check()` on the thread's project scope. -Same-org members can publish their own threads; cross-thread publish -is blocked (the tool resolves the thread's cwd from `threadId` and -refuses if the caller isn't the thread's owner or an org admin — -wire to existing thread ACL if present). - -Credential resolver: - -- Look up the acting user's GitHub token by `(userId, repo_host)`. -- If none, return `GITHUB_AUTH_MISSING` with the OAuth connect URL. -- Token scope needs `repo` (or `public_repo` if the target is public). - -Small companion endpoint for UI polish (not blocking): - -- `THREAD_STATUS` — returns `{ dirty: boolean, ahead: number, behind: - number, branch: string }` by calling `git status --porcelain` and - `git rev-list --left-right` in the worktree. UI uses it to show a - dirty indicator and to gate the Publish button. - -## UI - -Out of scope for this plan beyond listing the touchpoints: - -- Thread sidebar: "Publish" button, dirty indicator, last-pushed - branch URL. -- Publish dialog: commit message (prefilled with thread title), PR - checkbox, PR title/body (prefilled from thread). -- Post-publish: toast with branch URL, optional PR URL. -- GitHub connect flow when `GITHUB_AUTH_MISSING`. Reuse whatever - OAuth connect screen mesh already has for other providers. - -## File-by-file touchpoints - -- `packages/mesh-plugin-user-sandbox/server/runner/docker.ts` — drop - `gitIdentityScript` call in `bootstrapRepo` (line 885); add - `git remote set-url origin ` after the clone succeeds. -- `packages/mesh-plugin-user-sandbox/server/runner/types.ts` — add - optional `env` to `ExecOptions` if not already there. -- `packages/mesh-plugin-user-sandbox/server/runner/freestyle.ts` — - plumb `env` through to its exec equivalent. -- `packages/mesh-plugin-user-sandbox/image/daemon/http-helpers.mjs` — - confirm `/bash` passes `env` to the spawned child. -- `packages/mesh-plugin-user-sandbox/image/daemon/` — new - `credentials.mjs` for `POST /credentials`; router wire-up in - `daemon.mjs`. -- `packages/mesh-plugin-user-sandbox/image/Dockerfile` — install the - askpass helper script (`/usr/local/bin/askpass`) with `0755`. -- `packages/mesh-plugin-user-sandbox/image/askpass.sh` — new file, - tiny shell script that POSTs to the daemon's `/credentials` with - `DECO_EXEC_TOKEN`. -- `packages/mesh-plugin-user-sandbox/server/ensure-worktree.ts` — - swap `thread/` for the human-friendly branch naming in § - Branch naming. Sanitization helper. -- `apps/mesh/src/tools/...` — new `thread-publish.ts` (or wherever - thread-scoped tools live). `defineTool` with the input/output above. -- `apps/mesh/src/api/routes/...` — new `/api/sandbox/credentials` - endpoint that validates the exec token, resolves the user's - GitHub token, returns it. HMAC key shared with the sandbox token - minter. -- `apps/mesh/src/...` — GitHub PR creation helper (Octokit). Only - used if `openPr: true`. May already exist for other features. - -## Work breakdown - -Rough sizes, roughly ordered by dependency. - -1. Per-exec identity env plumbing: daemon `/bash` accepts `env`, - runner `exec` forwards, `bootstrapRepo` drops global config. (S) -2. `bootstrapRepo` origin URL rewrite. (XS) -3. Human-friendly branch naming in `ensureThreadWorkspace`. (XS) -4. Exec token HMAC mint + verify. Shared between mesh and the - daemon's `/credentials` handler. (S) -5. `POST /credentials` in daemon; askpass helper in image; - Dockerfile install. (S) -6. Mesh `/api/sandbox/credentials` endpoint: verify exec token, - resolve user's GitHub token from vault, return. (S) -7. `THREAD_PUBLISH` mesh tool: compose the push `/bash` call with - all the env. (S) -8. PR creation helper (Octokit) + wire into `THREAD_PUBLISH`. (S) -9. `THREAD_STATUS` tool (status + ahead/behind). (XS) -10. UI: publish button, dialog, dirty indicator. (M — separate PR) - -Steps 1–3 land independently of the rest; they're cleanups that pay -off on their own even if push never ships. 4–7 are the meat of the -feature. 8 is optional-at-first. 9–10 are polish. - -## Open questions to resolve before shipping - -- **Where does the acting user's GitHub token come from?** If mesh - has a per-user OAuth grant → straightforward. If only an org-level - app install exists → PRs get attributed to the app, with - `GIT_COMMITTER_*` carrying the human. Needs a look at what's in - `apps/mesh/src/` for GitHub integration today. -- **Clone URL tokens — where does the initial short-lived clone - token come from?** If we're using the same user OAuth token, the - clone is already authenticated and the "rewrite origin after - clone" step still matters (so the token doesn't survive in - `.git/config`). Confirm clone flow with whoever owns - `bootstrapRepo` originally. -- **Force-with-lease semantics under concurrent thread edits**: two - threads on the same agent won't touch each other's branches - (distinct `thread/...` names), so the classic force-push footgun - doesn't apply. But a single thread with two publish clicks - racing could — sequence the publish tool in mesh so concurrent - calls for the same thread queue instead of racing. -- **Exec token scope**: `(userId, sandboxHandle, threadId)`? - `(userId, sandboxHandle)`? Narrower is safer; wider is simpler. - I'd start narrower — bind to thread — and widen only if a real - use case emerges. -- **Askpass helper language**: shell is simplest; node gives better - error handling but adds a dep. Shell with `curl -sSf` is fine. - Confirm `curl` is in the base image (it is in most distros, but - `Dockerfile` should pin). -- **Commit author for the auto-`git add -A` commit when there's - untracked junk the user didn't mean to stage**: worth a - `.gitignore` check? Probably. A `node_modules` symlink shouldn't - end up committed, but a stray `.env` could. Mitigation: daemon's - `ensureThreadWorkspace` could ensure a sane `.gitignore` exists, - or the publish tool could refuse to push if obvious secrets - patterns are in the diff. Start permissive, add guardrails - driven by real near-misses. - -## Interactions with other plans - -- **PLAN.md (k8s move)**: per-exec identity (§Identity) and ASKPASS - callback (§Auth) are already listed in PLAN.md's work breakdown - items 6 and 7. This plan fleshes them out and extends them into - the actual push flow. No conflicts. -- **PLAN-PER-THREAD-DEV.md**: each thread already has its own - worktree and dev process. Push is scoped to the worktree's cwd, - so no new interaction — `THREAD_PUBLISH` resolves the worktree - via `ensureThreadWorkspace` and calls `/bash` with that cwd, - same as any other tool. -- **Sandbox identity** is unaffected. A thread's branch and commit - are thread state, not sandbox state. `(org, agent)` stays the - container key; the branch name is a per-thread property and - GitHub is the durable store for it. - -## Not in this plan - -- Auto-push on idle or on "task complete" heuristics. Explicit - trigger by default per the decision above. -- Rebasing thread branches onto default branch. Can be a separate - tool (`THREAD_REBASE`) that runs the rebase via `/bash` in the - worktree. No new infra needed. -- Multi-file commit curation (pick which hunks to include). The - publish is all-or-nothing on the worktree's dirty state for v1. -- Non-GitHub remotes. Provider-specific — revisit when a concrete - ask lands. -- Credential rotation beyond the per-exec token. User-scope token - rotation is the OAuth provider's job. diff --git a/packages/mesh-plugin-user-sandbox/PLAN-K8S-MVP.md b/packages/mesh-plugin-user-sandbox/PLAN-K8S-MVP.md deleted file mode 100644 index 81e859d2ff..0000000000 --- a/packages/mesh-plugin-user-sandbox/PLAN-K8S-MVP.md +++ /dev/null @@ -1,421 +0,0 @@ -# K8s Sandbox MVP - -Supersedes the k8s sections of `PLAN.md`. Local-kind-first; staging follows -only after the kind loop is green. Narrower scope than PLAN.md, grounded in -the admin reference impl at `deco-cx/admin/clients/agent-sandbox/` and the -existing Docker runner. - -## Goal - -Ship `KubernetesSandboxRunner` behind `MESH_SANDBOX_RUNNER=kubernetes`. The -validation path is local kind → deco staging → deco prod canary. Docker stays -the dev / self-host runner. Freestyle keeps working until the k8s path is -validated in prod, then gets removed in a follow-up. - -## Invariants (must hold across every stage) - -1. **One daemon binary, all runners.** `image/daemon.mjs` + `image/daemon/*.mjs` - is runner-agnostic. It binds `$DAEMON_PORT=9000`, authenticates via - `DAEMON_TOKEN`, and exposes every runtime surface under `/_daemon/*`. K8s - reuses the exact image Docker uses. No k8s-specific daemon fork — if we - need something for k8s, we add it to the daemon and Docker benefits too. - -2. **No `kubectl apply` or `kubectl ` from an operator's hands.** - Cluster state (operator, namespace, template, RBAC, HTTPRoute listener) is - Helm/Terraform. Per-sandbox state (SandboxClaim, per-claim Service) is K8s - API calls from mesh code via `@kubernetes/client-node`. `kubectl` may appear - in CI smoke-test scripts; it does not appear in runbooks. - -3. **Ref shape unchanged.** `projectRef = "agent:::"`; - composed via `composeSandboxRef()` in `server/runner/sandbox-ref.ts`. K8s - runner uses this to derive a deterministic claim name. - -## Decisions (locked before implementation) - -1. **Per-user**, not per-org. `sandbox_runner_state` PK is - `(user_id, project_ref, runner_kind)`; unchanged from Stage 0. - -2. **Per-branch pods**, not git-worktrees. Each `(user, virtualMcp, branch)` - gets its own claim → its own pod. Matches Docker and Freestyle. - -3. **Single shared namespace for MVP**: `agent-sandbox-system`. All - SandboxClaims + the single shared SandboxTemplate live here. This is the - agent-sandbox operator's expected topology (claim must be in the same ns - as its template) and matches `deco-cx/admin/hosting/kubernetes/common/envs/sandboxclaim.ts:29`. - Tenancy is enforced at the mesh layer: claim names are - `sha256(userId + ":" + projectRef).slice(0,16)`, unguessable from other - users. - - **Deferred**: per-org namespaces, NetworkPolicy, ResourceQuota. See - "Deferred hardening". The single-ns model bets on operator correctness; - we accept that bet for MVP because admin already runs this way. - -4. **No activator** in front of Ingress. Mesh already calls `runner.ensure` - on every code-initiated request; that IS the activator. K8s also gets - idle reap for free: `ensure()` refreshes `spec.lifecycle.shutdownTime` - on every hit, and the operator deletes claims whose deadline has - passed (see Stage 2.2). Next request after a reap just re-provisions. - -5. **emptyDir workdir for MVP.** First-touch cost (clone + install) paid - per recreate. Measure, then — only then — decide on PVC/EFS/snapshot. - See "Deferred optimizations". - -6. **Ingress via Istio Gateway API** on deco clusters. For local kind, - preview URLs are served through `kubectl port-forward`, matching admin's - off-cluster dev pattern at `sandboxclaim.ts:141`. No HTTPRoute locally. - -7. **agent-sandbox CRDs**: - - SandboxClaim: `extensions.agents.x-k8s.io/v1alpha1`, plural `sandboxclaims`. - - Sandbox: `agents.x-k8s.io/v1alpha1`, plural `sandboxes`. - - Pod name discovered via annotation `agents.x-k8s.io/pod-name` on the - Sandbox resource. - - Readiness: watch Sandbox, look for `status.conditions[?(@.type=="Ready")].status=="True"`. - - `spec.lifecycle.shutdownPolicy: "Delete"` on claims so idle reap frees storage. - - These match the admin reference at `clients/agent-sandbox/types.ts`. Confirm - against upstream `v0.4.x` during Stage 1 bring-up and pin in a constants file. - -8. **Image registry**: GHCR public (`ghcr.io//mesh-sandbox:`). - Matches the existing deco pattern (`ghcr.io/decocms/mcps/bun`, - `ghcr.io/deco-cx/deco`) — anonymous pulls, no `imagePullSecret`, no IRSA. - Self-hosters can pull the same image. **Confirm with infra before Stage 3.** - -## Stage 0 — landed - -Shipped on `wip/sandbox-no-claude`. Multi-runner interface, Freestyle as a -first-class `SandboxRunner`, k8s purely additive against this seam. - -### Interface (`server/runner/types.ts`) - -```ts -interface SandboxRunner { - readonly kind: "docker" | "freestyle"; // widen for k8s - - ensure(id, opts): Promise; - exec(handle, input): Promise; - delete(handle): Promise; - alive(handle): Promise; - - getPreviewUrl(handle): Promise; - proxyDaemonRequest(handle, path, init): Promise; -} -``` - -`resolveDevPort`/`resolveDaemonPort` are Docker-only. `sweepOrphans` is -Docker-only (mesh process owns `--rm` containers; Freestyle/K8s sandboxes -are independently managed). K8s idle reap is claim-side: `ensure()` bumps -`spec.lifecycle.shutdownTime` on every hit and the operator deletes claims -whose deadline has passed — see Stage 2. - -### Identity (`server/runner/sandbox-ref.ts`) - -`composeSandboxRef({ orgId, virtualMcpId, branch })` → `agent:::`. -K8s runner takes this plus `userId` and derives: - -```ts -const claimName = `mesh-sb-${sha256(userId + ":" + projectRef).slice(0, 16)}`; -``` - -### Per-kind dispatch (`apps/mesh/src/sandbox/lifecycle.ts`) - -- `getSharedRunner(ctx)`: env-active runner via `resolveRunnerKindFromEnv()`. -- `getRunnerByKind(ctx, kind)`: lazy-creates per-kind singletons. `VM_DELETE` - uses this so teardown follows the entry's recorded `runnerKind`. -- `asDockerRunner`: narrows for Docker-only ingress. - ---- - -## Stage 1 — local kind iteration - -**Objective**: prove the `SandboxClaim → pod → daemon /health → /_daemon/bash -→ /_daemon/dev/start → preview URL` loop end-to-end, against a real k8s API, -before touching any deco infrastructure. All mesh-side k8s code is written -and tested here. - -### 1.1 Cluster bring-up (Helm-only, scripted) - -`deploy/k8s-sandbox/local/` contains: - -- `Taskfile.yml` or `Makefile` with `up` / `down` / `reload-image` targets. - These targets call `kind`, `helm`, and `kind load docker-image`. They do - not call `kubectl apply` on any manifest. -- `values-local.yaml` — Helm values for the parent chart, pinning image = - `mesh-sandbox:local`, `imagePullPolicy: Never`. -- A parent Helm chart (`chart/`) that lists the upstream agent-sandbox - operator as a dependency (pinned to whatever upstream ships as - `v0.4.x`) and contributes: - - Namespace `agent-sandbox-system` (likely created by the upstream chart - already; dependency). - - A single `SandboxTemplate` resource matching the mesh sandbox image - (ports 9000 daemon + 3000 dev, `runAsNonRoot: true`, UID 1000 — - matches the Dockerfile's `USER sandbox`). - -`up` target flow: -1. `kind create cluster --name mesh-sandbox-dev` (idempotent: skip if exists). -2. `docker build -t mesh-sandbox:local packages/mesh-plugin-user-sandbox/image` - (same image Docker runner uses). -3. `kind load docker-image mesh-sandbox:local --name mesh-sandbox-dev`. -4. `helm upgrade --install mesh-sandbox ./deploy/k8s-sandbox/local/chart - -f ./deploy/k8s-sandbox/local/values-local.yaml --wait`. - -`down` is `kind delete cluster --name mesh-sandbox-dev`. `reload-image` is -step 2 + step 3 + `kubectl rollout restart` (the one exception to rule #2 — -it's a dev ergonomics script, not a runbook step). - -### 1.2 agent-sandbox client (port from admin) - -`packages/mesh-plugin-user-sandbox/server/runner/kube-client.ts`. Direct port -of `deco-cx/admin/clients/agent-sandbox/kubernetes.ts` and `types.ts`, but: - -- Uses `@kubernetes/client-node` (Node) not deno k8s deps. -- Constants file pins `K8S_CONSTANTS` from admin's `types.ts`. -- Exports: `createSandboxClaim`, `deleteSandboxClaim`, `getSandboxClaim`, - `waitForSandboxReady`. Same signatures, same watch-for-Ready semantics. - -This lives in the shared package (not `apps/mesh`) because it has no -heavy SDK — `@kubernetes/client-node` is already a reasonable dep for -sandbox-adjacent code, and keeping it in the package makes the runner -class co-locate with its HTTP client. - -### 1.3 `KubernetesSandboxRunner` - -**File**: `apps/mesh/src/sandbox/kubernetes-runner.ts`. Same rationale as -`freestyle-runner.ts`: heavy cluster-side state, keeps the shared package -lean. `kind: "kubernetes"`. - -**Claim identity**: -```ts -const CLAIM_NAMESPACE = "agent-sandbox-system"; -const claimName = `mesh-sb-${sha256(userId + ":" + projectRef).slice(0, 16)}`; -``` - -**Method mapping**: - -- `ensure(id, opts)`: - 1. Look up existing SandboxClaim by name. If `Ready`, rehydrate - `daemonUrl` from state and short-circuit. - 2. If missing, create the SandboxClaim (references the shared - `SandboxTemplate`). Wait for `Ready` via `waitForSandboxReady` - (watch-based, timeout 180s — same as admin). - 3. Read pod IP from the `agents.x-k8s.io/pod-name` annotation. - 4. **Local path**: spawn `kubectl port-forward` on an ephemeral port, - set `daemonUrl = http://127.0.0.1:`. Port-forward lifetime - = runner process lifetime (shutdown hook kills subprocess). Matches - admin's `deploySandbox` pattern. - 5. **In-cluster path** (Stage 3): `daemonUrl = http://:9000` - directly (mesh replicas live in the same cluster). - 6. Call `probeDaemonHealth(daemonUrl)` (already exists in `daemon-client.ts`). - 7. `bootstrapRepo` via `proxyDaemonRequest` — same code path as Docker. - 8. Persist to `sandbox_runner_state` with `runner_kind="kubernetes"`. - 9. Fire-and-forget `/_daemon/dev/start` — same as Docker. - 10. Return `{ handle: claimName, workdir: "/app", previewUrl: ... }`. - -- `exec(handle, input)`: `daemonBash(daemonUrl, token, input)` — identical - to Docker. The daemon is the daemon. - -- `proxyDaemonRequest(handle, path, init)`: `proxyDaemonRequest(daemonUrl, - token, path, init)` — identical to Docker. - -- `delete(handle)`: delete SandboxClaim (operator GCs pod). Remove state row. - Kill local port-forward if running. - -- `alive(handle)`: read Sandbox resource, check Ready condition. - -- `getPreviewUrl(handle)`: - - **Local**: `http://127.0.0.1:/`. Port-forward - for 3000 is separate from the daemon port-forward. - - **In-cluster**: `https://.sandboxes.decocms.com/` (Stage 3). - -**Daemon token**: the SandboxTemplate's pod spec must include -`DAEMON_TOKEN` as an env var. Two options: -- (a) Hard-code a token per template (all pods share one token — bad, one - leak compromises every sandbox). -- (b) Template omits the token; operator ignores it; mesh creates a K8s - Secret per claim (named after the claim), and the template references - the Secret by a convention like `$(CLAIM_NAME)-token`. - -Option (b) is the right answer. Spec'd out in Stage 2. - -**What's different from Docker, summarized**: - -| Concern | Docker | Kubernetes | -|--|--|--| -| Pod/container create | `docker run` | K8s API: create `SandboxClaim` | -| Daemon URL | `http://127.0.0.1:` (published port) | Local: `kubectl port-forward`. Cluster: `http://:9000` | -| Repo bootstrap | `proxyDaemonRequest("/_daemon/repo/init")` | **identical** | -| `exec` | `daemonBash(url, token, input)` | **identical** | -| Preview URL | `http://.sandboxes.localhost:7070/` | Local: port-forward. Cluster: `https://.sandboxes.decocms.com/` | -| Teardown | `docker stop` | K8s API: delete `SandboxClaim` | -| File copy | `docker cp` (used nowhere in-runner) | N/A — credentials go as pod env at provision, not runtime copy | -| Orphan sweep | mesh boot/shutdown (`--rm` containers) | Operator-side: `spec.lifecycle.shutdownTime` refreshed on each `ensure()`; operator reaps on expiry | - -### 1.4 Wire-up (4 file edits) - -- `server/runner/types.ts:79` — `readonly kind: "docker" | "freestyle" | "kubernetes"`. -- `server/runner/index.ts:50` — `export type RunnerKind = "docker" | "freestyle" | "kubernetes"`. -- `server/runner/index.ts:91` — `resolveRunnerKindFromEnv()`: accept - `"kubernetes"` in the explicit-value branch. -- `apps/mesh/src/sandbox/lifecycle.ts:25` — add `case "kubernetes"` that - dynamic-imports `./kubernetes-runner` and returns - `new KubernetesSandboxRunner({ stateStore })`. - -### 1.5 Tests - -- `kube-client.test.ts` — mock `@kubernetes/client-node` (same pattern - admin uses), verify claim payload shape, Ready watch termination, 404 - handling on delete. -- `kubernetes-runner.test.ts` — mock the kube client, mock daemon - transport; verify `ensure` short-circuits on Ready claim, creates on - missing claim, propagates `bootstrapRepo` errors correctly. -- **Integration smoke test** (not in CI by default): `deploy/k8s-sandbox/local/smoke.ts` - runs against the live kind cluster via `bun test`. Exercises: ensure → - exec → preview fetch → delete → recreate (cold) → ensure (warm) → - alive → delete. Blocks Stage 2 landing. - -### 1.6 Stage 1 exit criteria - -- `bun test packages/mesh-plugin-user-sandbox apps/mesh/src/sandbox` green. -- `deploy/k8s-sandbox/local/smoke.ts` green against kind. -- `MESH_SANDBOX_RUNNER=kubernetes bun run --cwd=apps/mesh dev` boots mesh - locally, a real `VM_START` call from the studio UI lands a pod in kind - and the preview iframe renders. -- No existing Docker or Freestyle test regresses. - -**Commits on this stage**: -1. `feat(sandbox): add kubernetes client + CRD constants (port from admin)` -2. `feat(sandbox): add KubernetesSandboxRunner behind MESH_SANDBOX_RUNNER=kubernetes` -3. `chore(sandbox): add deploy/k8s-sandbox/local kind bring-up chart` - ---- - -## Stage 2 — hardening (still local-ish; optional pre-staging work) - -Only after Stage 1 is green. Each item is independently deferrable. - -1. **Per-claim daemon token**: mesh creates a K8s `Secret` named - `-token` with a random token at claim-provision time. The - SandboxTemplate references it via `envFrom` / `valueFrom.secretKeyRef`. - Mesh reads the token back into `sandbox_runner_state` after - `waitForSandboxReady`. Matches the per-container token model Docker - already has. - -2. **Claim-side TTL for idle reap**: every `ensure()` hit patches - `spec.lifecycle.shutdownTime = now + idleTtlMs` (default 1h). With - `shutdownPolicy: Delete`, the operator GCs the claim (and pod) once - wall clock passes the deadline. No mesh-side loop, no cron, no leader - election — the operator is already reconciling every claim. - - The two drift directions that would have motivated a reconcile loop - self-heal on next access: - - State row exists, claim gone → `rehydrate()` gets 404, deletes the - row, fresh-provisions. - - Claim exists, no row → `adopt()` either rebuilds a record or deletes - the orphan and reprovisions. - - Neither case needs a background sweep. - -3. **Image-build CI** pushing `ghcr.io//mesh-sandbox:` on every - merge to main. Tag scheme TBD — likely `sha-` + `latest`. - -**Commits**: one per item, independently mergeable. - ---- - -## Stage 3 — staging rollout on EKS (`deco-mcp-mesh-stg`) - -Deco-cx infrastructure work; mesh code is unchanged from Stage 1/2. - -### 3.1 Cluster prereqs (Terraform / Helm; no `kubectl`) - -1. **Install agent-sandbox operator** into staging via the same Helm chart - used locally, parameterized with `values-staging.yaml`. -2. **Namespace**: `agent-sandbox-system` (shared with local). Created by - the operator's upstream chart. -3. **SandboxTemplate** pointing at `ghcr.io//mesh-sandbox:`, - `imagePullPolicy: IfNotPresent`, resources matching prod shape - (`requests: {cpu: 250m, memory: 512Mi}`, `limits: {cpu: 2, memory: 4Gi}`), - securityContext hardened (`runAsNonRoot: true`, drop `ALL`, seccomp - `RuntimeDefault`, `automountServiceAccountToken: false`). -4. **Ingress**: one new listener on the existing `istio-gateway-api-default` - Gateway for `*.sandboxes-stg.decocms.com` with a cert from - `decocms-ca-issuer`. Added to Terraform alongside the existing - `studio-stg` listener. -5. **Per-claim routing**: mesh creates `Service` (ClusterIP, selector on - the pod's agent-sandbox label) + `HTTPRoute` (host - `.sandboxes-stg.decocms.com` → Service port 3000) at - `ensure()` time; deletes them at `delete()` time. These are the only - runtime-created resources besides the SandboxClaim itself. -6. **Mesh RBAC**: the mesh ServiceAccount gets, via Terraform: - - `create/get/list/watch/delete` on `sandboxclaims` in `agent-sandbox-system`, - - `get/list/watch` on `sandboxes` + `pods`, - - `create/get/update/delete` on `services` + `httproutes` + `secrets` - in `agent-sandbox-system`. -7. **Dedicated node pool**: karpenter provisioner tainted - `mesh-sandbox=true:NoSchedule` so user code doesn't land on system - nodes. Mesh replicas don't tolerate. - -### 3.2 Mesh-side changes - -- `KubernetesSandboxRunner` gains an in-cluster mode: detects - `KUBERNETES_SERVICE_HOST` env var, uses in-cluster kubeconfig, skips - port-forward, sets `daemonUrl = http://:9000`. Flag: - `K8S_RUNNER_MODE=in-cluster|port-forward`; auto-detected by default. -- Per-claim Service + HTTPRoute CRUD added to `ensure`/`delete`. - -### 3.3 Validation - -1. Flip `deco-mcp-mesh-stg` env to `MESH_SANDBOX_RUNNER=kubernetes`. -2. Run ≥20 real thread spawns across ≥3 different virtualMcp/branch pairs. -3. Exercise: `VM_START`, bash tool first-exec, preview iframe load, dev - server HMR, `VM_STOP`, idle reap + rehydrate. -4. Compare latencies against Docker baseline. Targets: cold-start within - 2× Docker; warm within 20%. Error-rate parity with Docker or better. - -Only then do we pick what (if anything) from the Deferred Optimizations -list lands. - ---- - -## Deferred hardening (only if/when justified by measurement or incident) - -- **Per-org namespaces** + `NetworkPolicy` (deny-all default, allow mesh - + istio ingress) + `ResourceQuota`. Switches tenancy from mesh-claim-name - hashing to K8s primitives. Requires cloning SandboxTemplate per ns, or a - template-copying admission controller. Do this when (a) audit team asks, - or (b) a CVE in the operator lets one pod touch another's. -- **PVC-per-branch**, **EFS shared cache**, **VolumeSnapshot per commit**, - **warm pod pool**. Each addresses cold-start latency. Pick based on - Stage 3 measurements. -- **gVisor**. Stronger isolation, measurable `bun install` overhead. -- **Cross-user sandbox sharing inside org**. PK migration + per-exec - credential injection. Justify by measured multi-user-org cost. - ---- - -## Non-goals for this MVP (explicit) - -- Firecracker, Kata, or any microVM runtime. -- PVC / VolumeSnapshot storage model. -- gVisor. -- Per-exec git credential injection (GIT_ASKPASS callback to mesh). -- Warm pool. -- Cross-user sandbox sharing. -- Freestyle removal (happens after k8s prod canary, in its own PR). -- `bunx decocms` local-fallback (separate plan). - ---- - -## Rollout - -1. Stage 0 lands on main (done). Docker/Freestyle keep working. -2. Stage 1 lands on main behind `MESH_SANDBOX_RUNNER=kubernetes`. Default - env stays `docker` everywhere. Kind is devex-only. -3. Stage 2 items land independently as needed for Stage 3. -4. Stage 3 infra applied via Terraform/Helm. Stage 3 runner mode (in-cluster - detection + HTTPRoute) lands on main. -5. Staging flipped to `kubernetes`. Validate for 2 weeks. -6. Prod canary for a single internal org. Validate for 2 weeks. -7. Default flipped to `kubernetes` for all orgs. Freestyle removed in a - follow-up PR. - -Docker runner stays forever — it's the dev / self-host path. diff --git a/packages/mesh-plugin-user-sandbox/PLAN-K8S-REMAINING.md b/packages/mesh-plugin-user-sandbox/PLAN-K8S-REMAINING.md deleted file mode 100644 index 488f794164..0000000000 --- a/packages/mesh-plugin-user-sandbox/PLAN-K8S-REMAINING.md +++ /dev/null @@ -1,486 +0,0 @@ -# K8s Sandbox — Remaining Work - -Supersedes `PLAN-K8S-MVP.md` for forward planning. The MVP plan's Stage 0 -and most of Stage 1 have landed; this plan reflects what's actually left -plus the gaps the MVP plan hand-waved. - -Upstream baseline: `kubernetes-sigs/agent-sandbox v0.4.2` (pinned in -`deploy/k8s-sandbox/local/up.sh`). API groups: -`agents.x-k8s.io/v1alpha1` (Sandbox, SandboxWarmPool) and -`extensions.agents.x-k8s.io/v1alpha1` (SandboxClaim, SandboxTemplate). - -## What's already landed - -From `PLAN-K8S-MVP.md` Stages 0–1 and partial Stage 2: - -- `SandboxRunner` interface with kind widened to `"docker" | "freestyle" | "kubernetes"`. -- `server/runner/k8s/` — `runner.ts` (812 lines), `client.ts` + tests (924 lines), - `constants.ts` pinned to v1alpha1. -- `deploy/k8s-sandbox/local/` — `up.sh` / `down.sh` / `reload-image.sh`, - `sandbox-template.yaml`, `smoke.ts` exit criterion. -- Per-claim `DAEMON_TOKEN` via `SandboxClaim.spec.env` (no shared token in - the template). -- Claim-side idle reap via `spec.lifecycle.shutdownTime` refreshed on - every `ensure()`; operator GCs on expiry. -- `MESH_SANDBOX_RUNNER=kubernetes` dispatch wired through - `apps/mesh/src/sandbox/lifecycle.ts`. - -Everything below is what the MVP plan punted on, misspec'd, or genuinely -needs to happen before staging/prod. - ---- - -## Stage 2 — blockers for staging rollout - -Each item is independently mergeable. All of these must land before -Stage 3; none are optional. - -### 2.1 NetworkPolicy — P0, not "deferred hardening" - -**Rationale.** The workload is arbitrary user code. Without egress -restriction a sandbox pod can reach: - -- IMDSv2 at `169.254.169.254` — on EKS, the node's IAM role is a - cluster-takeover primitive. -- In-cluster services in other namespaces — mesh's Postgres, NATS, any - internal admin surface. -- Kubelet, metrics server, CoreDNS (beyond the DNS we want it to use). - -The MVP plan calls this "deferred … do this when audit asks." That was -wrong. Ship this with Stage 3, not after an incident. - -**Shape.** - -- One `NetworkPolicy` in `agent-sandbox-system` selecting pods labeled - `app.kubernetes.io/name: mesh-sandbox`: - - Ingress: allow from mesh replica pods (label selector) on port 9000 - (daemon), and from the Istio gateway namespace on port 3000 (dev - server). - - Egress: deny-all, then allow: - - DNS to CoreDNS (`kube-system`, UDP/TCP 53). - - Public internet on 443/80 (for `bun install`, git clones, etc.) - — but block RFC1918 + `169.254.0.0/16` + `fd00::/8` + `fe80::/10`. - Expressed as `to: - ipBlock: 0.0.0.0/0, except: [169.254.0.0/16, - 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10]`. - - Mesh API for daemon callbacks (by label, port 8000) — **only if** - we keep any callback channel. If daemon is pure pull, skip. -- Verify IMDS block with a smoke test: exec `curl -s --max-time 2 - http://169.254.169.254/latest/meta-data/iam/security-credentials/` - from inside a sandbox and assert non-zero exit. - -**Not sufficient alone.** Also set `hostNetwork: false` (default), -`dnsPolicy: ClusterFirst`, and on EKS enforce IMDSv2 hop-limit=1 at the -node level so even an egress miss can't land creds. The hop-limit -change is infra-repo work; note it on the Terraform PR. - -### 2.2 Credentials flow to the sandbox — P0 - -The MVP plan has one table cell: "credentials go as pod env at -provision, not runtime copy." That's not a design. Docker runner passes -git tokens and project env at container create; k8s must do the -equivalent and the plan doesn't say how. - -**Decision.** Per-claim Secret, referenced by the claim's `spec.env` -via `valueFrom.secretKeyRef`. Not injected into the template (template -is shared; secrets are per-sandbox). - -**Open question: the SandboxClaim CRD.** As of v0.4.2 the claim's -`spec.env` accepts literal `{name, value}` only — see -`deploy/k8s-sandbox/local/sandbox-template.yaml` and the README note. -That's why `DAEMON_TOKEN` ships as plaintext in the claim today. For -git tokens that's not acceptable. - -Two paths: - -1. **Template-side `envFrom: secretRef`** with a predictable name like - `-creds`. Mesh creates the Secret before creating the - claim. Needs template support for parameterized secret names — check - v0.4.2 CRD schema; may need a template-per-tenant-class or an - upstream patch. -2. **Operator-side env-merge from Secret** via an annotation on the - claim like `mesh.decocms.com/creds-secret: `, read by a tiny - mesh-side mutating webhook that rewrites the pod spec. Heavier; only - if path (1) isn't achievable. - -Path (1) first. If upstream CRD blocks it, open an issue and fall back -to the plaintext-in-claim model for tokens scoped tight enough that -leakage is survivable (short-lived GitHub installation tokens, not PATs). - -**OwnerReferences.** The per-claim Secret's `ownerReferences` point at -the SandboxClaim. When the operator reaps the claim on `shutdownTime`, -the Secret is GC'd automatically. Same pattern applies to every -auxiliary resource mesh creates (see 2.4). - -### 2.3 Concurrent `ensure()` across mesh replicas — P0 - -Two replicas hit `ensure()` for the same `(user, projectRef)` -simultaneously; both GET → 404 → POST → one 201, one 409. Current -runner.ts doesn't handle this (grepped: no 409 retry). - -**Fix.** In `createSandboxClaim`, catch `StatusError` with `code === 409`, -re-GET the claim, and proceed as if we'd seen it on the first GET. -Idempotent by construction because the claim name is deterministic -(`mesh-sb-<16-hex>`). - -Applies the same way to the per-claim Secret (2.2), Service, and -HTTPRoute (2.4). - -### 2.4 HTTPRoute + per-claim Service GC — P0 (for Stage 3) - -Stage 3 creates per-claim `Service` + `HTTPRoute` on `ensure()` and -deletes on `runner.delete()`. But `runner.delete()` is not the only -teardown path: the operator reaps the claim on idle expiry without -touching the Service/HTTPRoute. Orphan accumulation over days. - -**Fix.** Set `ownerReferences` on the Service and HTTPRoute pointing at -the SandboxClaim. K8s GC chains: claim deleted → Service + HTTPRoute -deleted. Mesh's `delete()` becomes "delete the claim; everything else -follows." - -Verify with a smoke test that creates → waits for idle reap (set -`idleTtlMs` to 30s for the test) → asserts Service/HTTPRoute are gone. - -Upstream is aware per-claim Services are a scale issue — the project -README mentions "exploration of efficient traffic routing without -per-sandbox Services." Track it; migrate when upstream ships. - -### 2.5 Pod-ready vs daemon-ready — P0 - -The Sandbox CR's `Ready` condition means scheduler + kubelet probes -green. The daemon binds 9000 a few seconds later. Runner must gate on -`probeDaemonHealth` after `waitForSandboxReady`, with its own timeout -and retry. - -Grep `runner.ts` for the current gating — if there's no -`probeDaemonHealth` call between Ready and first proxy, add it with a -30s budget and exponential backoff (200ms → 2s). Classify a timeout -here as distinct from a claim-never-ready failure so we can alert -differently. - -### 2.6 Observability — P0 for staging debug - -When `waitForSandboxReady` times out in Stage 3, the only signals today -are the claim's status conditions. That's not enough to triage -("ImagePullBackOff"? "Insufficient CPU"? "Init container crash"?). - -**Must expose:** - -1. Events API read on timeout: `kubectl get events --field-selector - involvedObject.name=,involvedObject.kind=Sandbox`, last 10, - included in the error surface. -2. Pod logs fallback: if the daemon itself never comes up, mesh should - be able to fetch `pods/log` for the sandbox pod and include the last - N lines in the failure response. RBAC needs `get` on `pods/log` - (Stage 3.1 addition). -3. Runner-emitted OTEL spans for `ensure`, `waitForSandboxReady`, - `probeDaemonHealth`, `delete`. Labels: `runner_kind=kubernetes`, - `claim_name`, `namespace`. Flows into the existing mesh tracer. - -Upstream-exposed operator metrics per -`agent-sandbox.sigs.k8s.io/docs/sandbox/metrics/` are currently -client-side SDK metrics only; no operator Prometheus endpoint -documented. Don't depend on them. - -### 2.7 Multi-arch image build CI — P0 for staging - -Local builds on M-series laptops produce arm64; EKS node pools are -typically amd64. Shipping to GHCR needs `docker buildx build --platform -linux/amd64,linux/arm64 --push`. - -**Shape.** GitHub Actions workflow on push to main: - -- Path filter: `packages/mesh-plugin-user-sandbox/image/**`. -- `docker/setup-qemu-action` + `docker/setup-buildx-action`. -- Push `ghcr.io/decocms/mesh-sandbox:sha-` and `:latest`. -- Sign with cosign keyless (sigstore/cosign-installer) — the - SandboxTemplate's image reference can stay unsigned for MVP; signing - unlocks future `verify-image` admission. - -Coordinate the tag bump in infra repo's `values-staging.yaml` on each -rollout — do **not** use `:latest` in staging. - -### 2.8 Preview URL authorization — P0 decision, P1 implementation - -Today's plan: `.sandboxes-stg.decocms.com` → HTTPRoute → -ClusterIP Service → pod:3000. No auth. Anyone with the hostname hits -the dev server. Hostname is unguessable (16-char hash of -`userId:projectRef`), but that's secrecy-through-obscurity — anyone -the user shares the preview with also shares with the world -indefinitely. - -**Decision for MVP.** Accept unguessable-hostname as the only gate for -*staging*. Document it. Preview URLs are for the sandbox owner's own -iframe inside mesh; users don't share them externally. - -**Before prod canary.** Terminate previews on an auth proxy (mesh's -own gateway, or Istio AuthorizationPolicy with JWT from mesh session -cookie). Scope: only the session that owns the sandbox, or session -members of the same org. This is the right boundary for a multi-tenant -product. - -Track the upgrade as its own doc under `apps/mesh/src/sandbox/` before -prod flip. - -### 2.9 `emptyDir` sizeLimit — P1 - -A `dd if=/dev/zero of=/tmp/fill` fills the node's ephemeral storage -and evicts neighbors. Set `sizeLimit: 10Gi` (or tuned) on any emptyDir -volume in the template. Currently the template doesn't mount emptyDir -on `/app` (image's own writable layer is used, ephemeral to pod), but -if that changes — set the limit. - -Also set `ephemeral-storage` requests + limits on the container itself -so scheduler places it sensibly. Default ephemeral on most EKS AMIs is -the root volume; an unbounded sandbox can starve kubelet. - -### 2.11 OSS Helm packaging — P0 for self-hosters - -**Rationale.** Per `feedback_no_freestyle_mesh_must_be_oss`, both runners -stay first-class and OSS self-hosters need a viable k8s path. Today -`deploy/helm/Chart.yaml` (`chart-deco-studio` v0.6.2) has NATS + OTEL -collector as dependencies but no k8s sandbox components. A self-hoster -choosing `MESH_SANDBOX_RUNNER=kubernetes` has to install agent-sandbox -operator + SandboxTemplate + NetworkPolicy by hand. - -**Upstream constraint.** `kubernetes-sigs/agent-sandbox` v0.4.2 does NOT -publish a Helm chart. Release assets are `manifest.yaml` and -`extensions.yaml` only. So we can't list it under `dependencies:` in -`Chart.yaml` pointing at an upstream Helm repo. - -**Decision.** Local subchart at `deploy/helm/charts/agent-sandbox/`, -vendored from the upstream release YAML pinned to v0.4.2. Parent -chart references it with `repository: "file://./charts/agent-sandbox"` -and `condition: sandbox.kubernetes.enabled` so non-k8s self-hosters -aren't forced to install it. - -**Parent chart additions:** - -- `templates/sandbox-template.yaml` — prod-ceiling `SandboxTemplate` - (mirrors `deploy/k8s-sandbox/local/sandbox-template.yaml` with - configurable image ref, resources, securityContext). -- `templates/sandbox-network-policy.yaml` — the policy from 2.1, - templated with `{{ .Release.Namespace }}`. -- `templates/sandbox-warm-pool.yaml` — `SandboxWarmPool` skeleton - (disabled by default via `sandbox.kubernetes.warmPool.enabled`); one - value knob to set pool size. Ships disabled; self-hosters who hit - cold-start pain can flip it. -- `values.yaml` additions under a new `sandbox.kubernetes` key: - ```yaml - sandbox: - kubernetes: - enabled: false - image: - repository: ghcr.io/decocms/mesh-sandbox - tag: latest - pullPolicy: IfNotPresent - resources: - requests: { cpu: 500m, memory: 1Gi } - limits: { cpu: 2, memory: 4Gi, ephemeral-storage: 10Gi } - networkPolicy: - enabled: true - warmPool: - enabled: false - size: 0 - ``` - -**Version bumps.** When upstream releases vNext, a maintainer runs a -small script (`deploy/helm/charts/agent-sandbox/vendor.sh`) that -fetches the new release assets and overwrites the vendored YAML. Ship -the script; automation is future work. - -**Upstream contribution path.** Getting a proper Helm chart into -`kubernetes-sigs/agent-sandbox` upstream would delete our subchart -eventually. Open an issue linking to our subchart as prior art; not -blocking. - -### 2.10 Template upgrade path — P1 - -When we publish `mesh-sandbox:sha-` and update the -SandboxTemplate, existing claims keep the old image until they idle- -reap and get re-provisioned. That's acceptable for daemon-only -changes; it's *not* acceptable for security patches. - -**Mechanism for forced roll.** Mesh-side admin tool that lists all -claims in `agent-sandbox-system`, deletes each one; operator reaps; -next `ensure()` from the user re-creates against the new template. -Wired as a mesh tool gated on admin role, not a scheduled job. Users -lose their unsaved state — acceptable cost, rare event. - -**Note on the Docker runner.** Same problem, same answer: bump image, -next `docker run` picks it up. No new surface. - ---- - -## Stage 3 — staging rollout - -After every Stage 2 item is merged. Cluster is `deco-mcp-mesh-stg` -(EKS). - -### 3.1 Infra work (Terraform + Helm, in infra repo) - -All items here live in `decocms/infra_applications/provisioning/` or -similar — none ship from this repo. - -1. **Operator install.** Helm, pinned to v0.4.2 (match local). - Parent chart includes the upstream operator as a dependency; - upstream ships `manifest.yaml` + `extensions.yaml` so the parent - chart may just wrap kubectl-apply via a Job, or we vendor the YAML. - Verify whether v0.4.x publishes a proper Helm chart — if not, file - upstream and either wrap or vendor for now. -2. **Namespace.** `agent-sandbox-system`. Created by the operator - manifest. -3. **SandboxTemplate.** Ported from `deploy/k8s-sandbox/local/ - sandbox-template.yaml` with prod ceilings: - - `resources.requests: {cpu: 500m, memory: 1Gi}`. - - `resources.limits: {cpu: 2, memory: 4Gi, ephemeral-storage: 10Gi}`. - - `imagePullPolicy: IfNotPresent`. - - Image: `ghcr.io/decocms/mesh-sandbox:` (Stage 2.7 pipeline). - - `automountServiceAccountToken: false` — already set locally. - - `securityContext` — already hardened locally; mirror. -4. **Gateway listener.** One new listener on the existing - `istio-gateway-api-default` Gateway for - `*.sandboxes-stg.decocms.com` with a cert from `decocms-ca-issuer`. - Wildcard DNS record (`*.sandboxes-stg.decocms.com → ALB/NLB`) in - the same Terraform module. -5. **Mesh ServiceAccount RBAC.** One Role + RoleBinding in - `agent-sandbox-system`: - - `sandboxclaims` (`extensions.agents.x-k8s.io`): - `create/get/list/watch/delete/patch`. - - `sandboxes` + `pods` + `pods/log` + `events`: - `get/list/watch`. - - `services`: `create/get/update/delete`. - - `httproutes` (`gateway.networking.k8s.io`): - `create/get/update/delete`. - - `secrets`: `create/get/update/delete` (per-claim creds — 2.2). -6. **NetworkPolicy.** Applied via Helm from 2.1. -7. **Karpenter provisioner.** Tainted `mesh-sandbox=true:NoSchedule`. - Add matching toleration + `nodeSelector` on the **SandboxTemplate - pod spec** (not mesh pods). User code lands only on that pool. -8. **EKS node hop-limit=1** for IMDSv2 (2.1). - -### 3.2 Mesh-side changes - -- `KubernetesSandboxRunner` detects `KUBERNETES_SERVICE_HOST` and - switches to in-cluster kubeconfig: skip port-forward; `daemonUrl = - http://:9000`. Env override: `K8S_RUNNER_MODE=in-cluster| - port-forward` (auto by default). -- Per-claim Service + HTTPRoute create on `ensure()`, with - `ownerReferences` → SandboxClaim (Stage 2.4). -- Events + pod-logs fallback on `waitForSandboxReady` timeout - (Stage 2.6). - -### 3.3 Validation - -Flip `deco-mcp-mesh-stg` to `MESH_SANDBOX_RUNNER=kubernetes`, then: - -1. Internal smoke: ≥20 real thread spawns across ≥3 virtualMcp/branch - pairs. -2. Exercise every path — `VM_START`, bash tool first-exec, preview - iframe, HMR, `VM_STOP`, idle reap + rehydrate, Service+HTTPRoute - cleanup after reap. -3. Latency vs Docker baseline: cold ≤ 2×, warm ≤ 1.2×. Error rate - ≤ Docker. -4. Chaos: kill a random sandbox pod mid-request; verify next - `ensure()` reprovisions. Delete a mesh replica holding a - port-forward (if any leaked); verify no dangling forwards. -5. Soak: 2 weeks without intervention before prod canary. - ---- - -## Stage 4 — prod canary + Freestyle removal - -1. Enable `kubernetes` for one internal org in prod. Same infra shape - as staging. -2. Soak 2 weeks with real traffic. -3. Flip default `MESH_SANDBOX_RUNNER=kubernetes` for all orgs. -4. Freestyle runner removal happens in its own PR after the flip. Per - the `feedback_no_freestyle_mesh_must_be_oss` memory, freestyle stays - a supported option for self-hosters even post-flip. What "removal" - means in practice: drop freestyle as an available runner on - deco-hosted staging/prod; leave the code and - `MESH_SANDBOX_RUNNER=freestyle` path working for self-hosters. - Revisit with the user before any code deletion. - ---- - -## Stage 5 — state migration for existing rows - -After the default flip in Stage 4.3, existing `sandbox_runner_state` -rows have `runner_kind IN ("docker", "freestyle")`. Current dispatch -uses the recorded kind for `VM_DELETE` — so old sandboxes keep going -to their old runner forever unless we migrate. - -**Options:** - -1. **Let them drain.** Old rows age out via idle TTL; new `ensure()` - calls create new rows against `kubernetes`. Simple; leaves - long-lived freestyle sandboxes alive for some users indefinitely. -2. **Force-migrate.** On `ensure()`, if the recorded `runner_kind` - differs from the env-active kind and the row belongs to a - deco-hosted org, delete the old sandbox (via its recorded runner) - and re-provision on kubernetes. Users lose in-flight state. -3. **Hybrid.** Drain by default; offer a "move to k8s" tool for users - who want it; hard-migrate after N days. - -Recommend (3). Write it as an admin tool + scheduled drain, not a -synchronous check on every ensure (that would add a round trip to -every request). - ---- - -## Explicit non-goals for this plan - -- **Snapshots.** Upstream requires gVisor on GKE Autopilot - (`agent-sandbox.sigs.k8s.io/docs/sandbox/snapshots/`). We're on EKS. - Either migrate the sandbox pool to GKE (huge scope) or build our own - CSI-VolumeSnapshot + restore flow. Not in the critical path. Measure - cold-start pain in Stage 3 before taking this on. -- **SandboxWarmPool.** Upstream CRD exists. Useful if Stage 3 cold- - start is the dominant latency. Defer until measurement. -- **Per-org namespaces + ResourceQuota.** Still deferred, but the - rationale is different from the MVP plan: NetworkPolicy (2.1) covers - the tenancy boundary that matters. Quotas become relevant when one - user's `for i in {1..10000}; do ensure; done` starts hurting the - cluster — which isn't a tenancy break, it's rate-limiting. Handle at - the mesh layer (per-user concurrent sandbox cap) before it's a K8s - problem. -- **gVisor for sandbox isolation.** Different concern from snapshots. - Worth it on a shared node pool; less critical with dedicated pool + - NetworkPolicy. Re-evaluate after Stage 3. -- **Cross-user sandbox sharing.** Needs PK migration + per-exec - credential injection; already scoped out in the memory - (`project_thread_sandbox_ref`). - ---- - -## Open questions (need answers before Stage 3 PR) - -1. **CRD support for `envFrom: secretRef` in SandboxClaim spec.** - Confirm against v0.4.2 schema. If missing, Stage 2.2 path (1) needs - upstream work; fall back to (2) or patch upstream. -2. **Operator Helm chart vs raw manifests.** Does v0.4.x ship a Helm - chart? If not, decide: vendor YAML, or wrap `kubectl apply` in a - Helm post-install Job. -3. **Preview URL auth before prod.** Istio AuthorizationPolicy with - JWT from mesh session, vs a dedicated auth proxy pod per sandbox, - vs mesh-side reverse proxy. Infra preference? -4. **GHCR org.** `ghcr.io/decocms/mesh-sandbox` vs `ghcr.io/deco-cx/ - mesh-sandbox`. Today's pinned image name in the template is - `mesh-sandbox:local` — prod registry name TBD with infra. - ---- - -## Rollout sequence - -1. Stage 2.1–2.10 land on main, each as its own PR. Default env stays - `docker` everywhere. -2. Infra PR (Stage 3.1) lands in decocms/infra_applications. -3. Mesh Stage 3.2 lands on main behind the existing env flag. -4. Staging flipped to `kubernetes`. 2-week soak. -5. Prod canary (Stage 4.1) for one internal org. 2-week soak. -6. Default flipped (Stage 4.3). Migration tool (Stage 5) lands in - parallel. -7. Freestyle removal from deco-hosted — follow-up PR, revisit with - user first. - -Docker stays forever — dev + self-host path. From 035704c1c0cdd16c2d0abb9d1b6c987441dc0645 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Mon, 27 Apr 2026 21:55:02 -0300 Subject: [PATCH 05/16] Add .gitattributes for Helm chart and update README, reload-image.sh, smoke.ts, and up.sh scripts - Introduced .gitattributes to mark generated files for the Helm chart in agent-sandbox. - Updated README to clarify the build process for the daemon bundle and image. - Modified reload-image.sh and up.sh to build the daemon bundle before building the Docker image, ensuring the correct context is used. - Adjusted import paths in smoke.ts to reflect the new package structure. --- .gitattributes | 8 ++++++++ deploy/k8s-sandbox/local/README.md | 2 +- deploy/k8s-sandbox/local/reload-image.sh | 8 ++++++-- deploy/k8s-sandbox/local/smoke.ts | 10 +++++----- deploy/k8s-sandbox/local/up.sh | 15 +++++++++++---- 5 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..0b1d3e6ea0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Vendored upstream Helm chart for kubernetes-sigs/agent-sandbox. +# Refreshed via deploy/helm/charts/agent-sandbox/vendor.sh — do not hand-edit. +# Marking as generated so GitHub collapses the diff in PRs and excludes it +# from language stats; bumps are still reviewable by reading vendor.sh's +# version arg. +deploy/helm/charts/agent-sandbox/crds/** linguist-generated=true +deploy/helm/charts/agent-sandbox/templates/** linguist-generated=true +deploy/helm/charts/agent-sandbox-*.tgz binary linguist-generated=true diff --git a/deploy/k8s-sandbox/local/README.md b/deploy/k8s-sandbox/local/README.md index d1bd00ca4a..c1ebc2d391 100644 --- a/deploy/k8s-sandbox/local/README.md +++ b/deploy/k8s-sandbox/local/README.md @@ -39,7 +39,7 @@ Pins: 2. Applies the agent-sandbox `v0.4.2` base manifest (namespace, CRDs, controller) 3. Applies the agent-sandbox `v0.4.2` extensions manifest (SandboxClaim, SandboxTemplate, …) 4. Waits for controller deployments to report `Available` -5. Builds `packages/mesh-plugin-user-sandbox/image/` as `mesh-sandbox:local` +5. Builds the daemon bundle (`bun run --cwd packages/sandbox build`), then `packages/sandbox/image/Dockerfile` as `mesh-sandbox:local` 6. Loads the image into kind (required because the template pins `imagePullPolicy: Never`) 7. Applies `sandbox-template.yaml` diff --git a/deploy/k8s-sandbox/local/reload-image.sh b/deploy/k8s-sandbox/local/reload-image.sh index 84454abae0..249824b9b1 100755 --- a/deploy/k8s-sandbox/local/reload-image.sh +++ b/deploy/k8s-sandbox/local/reload-image.sh @@ -11,7 +11,8 @@ IMAGE_TAG="mesh-sandbox:local" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" -IMAGE_CONTEXT="${REPO_ROOT}/packages/mesh-plugin-user-sandbox/image" +SANDBOX_PKG="${REPO_ROOT}/packages/sandbox" +DOCKERFILE="${SANDBOX_PKG}/image/Dockerfile" KCTX="kind-${CLUSTER_NAME}" log() { printf "\033[1;34m[reload]\033[0m %s\n" "$*"; } @@ -21,8 +22,11 @@ if ! kind get clusters 2>/dev/null | grep -qx "${CLUSTER_NAME}"; then exit 1 fi +log "rebuilding daemon bundle" +bun run --cwd "${SANDBOX_PKG}" build + log "rebuilding ${IMAGE_TAG}" -docker build -t "${IMAGE_TAG}" "${IMAGE_CONTEXT}" +docker build -t "${IMAGE_TAG}" -f "${DOCKERFILE}" "${SANDBOX_PKG}" log "reloading ${IMAGE_TAG} into kind" kind load docker-image "${IMAGE_TAG}" --name "${CLUSTER_NAME}" diff --git a/deploy/k8s-sandbox/local/smoke.ts b/deploy/k8s-sandbox/local/smoke.ts index 1ca035acd5..84bcd8ef96 100644 --- a/deploy/k8s-sandbox/local/smoke.ts +++ b/deploy/k8s-sandbox/local/smoke.ts @@ -23,14 +23,14 @@ // Imported via relative paths, not the package export name: this script is // not inside any package, so bun would resolve module names from repo-root -// node_modules (which doesn't carry mesh-plugin-user-sandbox). Relative -// imports resolve @kubernetes/client-node from the package's own -// node_modules naturally. +// node_modules (which doesn't carry @decocms/sandbox). Relative imports +// resolve @kubernetes/client-node from the package's own node_modules +// naturally. import { KubeConfig, KubernetesSandboxRunner, -} from "../../../packages/mesh-plugin-user-sandbox/server/runner/k8s"; -import type { SandboxId } from "../../../packages/mesh-plugin-user-sandbox/server/runner/types"; +} from "../../../packages/sandbox/server/runner/k8s"; +import type { SandboxId } from "../../../packages/sandbox/server/runner/types"; const KIND_CONTEXT = "kind-mesh-sandbox-dev"; const NAMESPACE = "agent-sandbox-system"; diff --git a/deploy/k8s-sandbox/local/up.sh b/deploy/k8s-sandbox/local/up.sh index a06683163e..80dacdfe44 100755 --- a/deploy/k8s-sandbox/local/up.sh +++ b/deploy/k8s-sandbox/local/up.sh @@ -16,7 +16,8 @@ IMAGE_TAG="mesh-sandbox:local" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" -IMAGE_CONTEXT="${REPO_ROOT}/packages/mesh-plugin-user-sandbox/image" +SANDBOX_PKG="${REPO_ROOT}/packages/sandbox" +DOCKERFILE="${SANDBOX_PKG}/image/Dockerfile" MANIFEST_URL="https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${OPERATOR_VERSION}/manifest.yaml" EXTENSIONS_URL="https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${OPERATOR_VERSION}/extensions.yaml" @@ -49,9 +50,15 @@ kubectl --context "${KCTX}" wait \ --for=condition=Available deployment \ -n agent-sandbox-system --all --timeout=180s -# 5. build the sandbox image (same Dockerfile Docker runner uses) -log "building ${IMAGE_TAG} from ${IMAGE_CONTEXT}" -docker build -t "${IMAGE_TAG}" "${IMAGE_CONTEXT}" +# 5. build the sandbox image (same Dockerfile the Docker runner uses). +# The Dockerfile copies `daemon/dist/daemon.js` from the build context, so +# the daemon bundle has to be produced first and the build context has to +# be the sandbox package root (not image/). +log "building daemon bundle" +bun run --cwd "${SANDBOX_PKG}" build + +log "building ${IMAGE_TAG} from ${SANDBOX_PKG}" +docker build -t "${IMAGE_TAG}" -f "${DOCKERFILE}" "${SANDBOX_PKG}" # 6. load into kind so imagePullPolicy: Never resolves log "loading ${IMAGE_TAG} into kind cluster ${CLUSTER_NAME}" From ca4189364066661da2554ea7a67240023277f4ff Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Mon, 27 Apr 2026 22:13:20 -0300 Subject: [PATCH 06/16] Enhance logging in Broadcaster and refactor KubernetesSandboxRunner - Added stdout logging in the Broadcaster class to mirror SSE subscriber output for better visibility in `kubectl logs` and k9s. - Refactored KubernetesSandboxRunner to streamline port-forwarding logic, removing the devForward property and ensuring that preview traffic is routed through the daemon port-forward. - Updated comments for clarity on the daemon's role in handling traffic and the implications of the new structure. --- packages/sandbox/daemon/events/broadcast.ts | 4 + packages/sandbox/server/runner/k8s/runner.ts | 102 ++++++------------- 2 files changed, 35 insertions(+), 71 deletions(-) diff --git a/packages/sandbox/daemon/events/broadcast.ts b/packages/sandbox/daemon/events/broadcast.ts index 6bc359f6f1..a6dc023500 100644 --- a/packages/sandbox/daemon/events/broadcast.ts +++ b/packages/sandbox/daemon/events/broadcast.ts @@ -26,6 +26,10 @@ export class Broadcaster { broadcastChunk(source: string, data: string): void { if (!data) return; this.replay.append(source, data); + // Tee to stdout so `kubectl logs` / k9s show the same output that SSE + // subscribers see. The structured events (broadcastEvent below) stay + // SSE-only — they're machine-readable JSON and would be noise here. + process.stdout.write(`[${source}] ${data}`); const bytes = sseFormat("log", JSON.stringify({ source, data })); this.fan(bytes); } diff --git a/packages/sandbox/server/runner/k8s/runner.ts b/packages/sandbox/server/runner/k8s/runner.ts index cf4e7d2bc4..b30009f0d9 100644 --- a/packages/sandbox/server/runner/k8s/runner.ts +++ b/packages/sandbox/server/runner/k8s/runner.ts @@ -3,13 +3,19 @@ * * Provisions one SandboxClaim per (user, projectRef) against the * kubernetes-sigs/agent-sandbox operator. Mesh runs outside the cluster - * (Stage 1 / local-dev via kind), so daemon and dev traffic both reach the - * pod via a lazily-opened 127.0.0.1 TCP listener that tunnels each inbound - * connection through the apiserver as a fresh WebSocket. + * (Stage 1 / local-dev via kind), so traffic reaches the pod via a single + * lazily-opened 127.0.0.1 TCP listener that tunnels each inbound connection + * to the daemon container port through the apiserver as a fresh WebSocket. + * + * The daemon owns the public surface: it serves `/_decopilot_vm/*` + `/health` + * in-process and reverse-proxies everything else to in-pod localhost:DEV_PORT + * (CSP/X-Frame stripping + HMR bootstrap injection live in that proxy). One + * port-forward per pod is therefore enough; opening a second forwarder for + * the dev port would bypass the daemon and break SSE + iframe embedding. * * Stage 3 replaces the port-forward path with real ingress: when - * `previewUrlPattern` is set, no dev port-forward is opened and the preview - * URL is synthesized from the handle. + * `previewUrlPattern` is set, no forwarder is opened for preview traffic and + * the preview URL is synthesized from the handle. * * Token model: each claim carries a freshly-generated DAEMON_TOKEN injected * via `SandboxClaim.spec.env`. One leak compromises one sandbox. @@ -68,7 +74,11 @@ const DEFAULT_NAMESPACE = "agent-sandbox-system"; const DEFAULT_TEMPLATE_NAME = "mesh-sandbox"; const DAEMON_CONTAINER_PORT = 9000; -const DEV_CONTAINER_PORT = 3000; +// In-pod port the daemon's reverse proxy targets. Mesh never connects here +// directly — everything funnels through the daemon container port — but the +// value is propagated to the daemon via DEV_PORT so it knows where the dev +// server will bind. +const DEFAULT_DEV_PORT = 3000; const DEFAULT_WORKDIR = "/app"; // 32 bytes matches Docker's generation so audit logs don't vary by runner. @@ -114,7 +124,6 @@ interface K8sRecord { workdir: string; daemonUrl: string; daemonForward: PortForwarder; - devForward: PortForwarder | null; workload: Workload | null; /** * Per-boot UUID the daemon reports on /health. Generated mesh-side and @@ -203,10 +212,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { async delete(handle: string): Promise { const rec = await this.getRecord(handle); this.records.delete(handle); - if (rec) { - this.closeForwarder(rec.daemonForward); - if (rec.devForward) this.closeForwarder(rec.devForward); - } + if (rec) this.closeForwarder(rec.daemonForward); await deleteSandboxClaim(this.kubeConfig, this.namespace, handle); if (this.stateStore) { if (rec) await this.stateStore.delete(rec.id, RUNNER_KIND); @@ -226,21 +232,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { async getPreviewUrl(handle: string): Promise { const rec = await this.getRecord(handle); if (!rec) return null; - if (this.previewUrlPattern) { - return applyPreviewPattern(this.previewUrlPattern, handle); - } - // rehydrate() already opens devForward in local mode; if it's still null, - // the opener failed earlier and we try again now. - if (!rec.devForward) { - rec.devForward = await this.openForwarder( - rec.podName, - DEV_CONTAINER_PORT, - rec.handle, - ).catch(() => null); - } - return rec.devForward - ? `http://127.0.0.1:${rec.devForward.localPort}/` - : null; + return this.composePreviewUrl(rec); } async proxyDaemonRequest( @@ -355,7 +347,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { const token = this.tokenGenerator(); const daemonBootId = randomUUID(); const workdir = DEFAULT_WORKDIR; - const devContainerPort = opts.workload?.devPort ?? DEV_CONTAINER_PORT; + const devContainerPort = opts.workload?.devPort ?? DEFAULT_DEV_PORT; const runtime = opts.workload?.runtime ?? "node"; const packageManager = opts.workload?.packageManager ?? null; const repo = opts.repo ?? null; @@ -433,19 +425,6 @@ export class KubernetesSandboxRunner implements SandboxRunner { throw err; } - // Local-mode (Stage 1): eagerly open the dev port-forward so VM_START - // returns a preview URL that actually resolves, matching Docker. - const devForward = this.previewUrlPattern - ? null - : await this.openForwarder(podName, DEV_CONTAINER_PORT, handle).catch( - (err) => { - console.warn( - `[${LOG_LABEL}] dev port-forward for ${handle} failed: ${err instanceof Error ? err.message : String(err)}`, - ); - return null; - }, - ); - return { id, handle, @@ -454,7 +433,6 @@ export class KubernetesSandboxRunner implements SandboxRunner { workdir, daemonUrl, daemonForward, - devForward, workload: opts.workload ?? null, daemonBootId, }; @@ -462,9 +440,9 @@ export class KubernetesSandboxRunner implements SandboxRunner { /** * Reconstruct a record from persisted state. After this returns, the record - * is ready for any of the six methods — daemon + dev port-forwards are - * open (local mode). Returns null on any mismatch; caller purges and falls - * through to adopt/provision. + * is ready for any of the six methods — the daemon port-forward is open and + * its `/health` has been re-probed. Returns null on any mismatch; caller + * purges and falls through to adopt/provision. */ private async rehydrate( id: SandboxId, @@ -509,21 +487,6 @@ export class KubernetesSandboxRunner implements SandboxRunner { ); } - // Local-mode: warm the dev forwarder so `previewUrl` cached in vmMap - // resolves immediately after mesh restart. Stage 3 skips this. - const devForward = this.previewUrlPattern - ? null - : await this.openForwarder( - currentPodName, - DEV_CONTAINER_PORT, - handle, - ).catch((err) => { - console.warn( - `[${LOG_LABEL}] dev port-forward for ${handle} failed: ${err instanceof Error ? err.message : String(err)}`, - ); - return null; - }); - return { id, handle, @@ -532,7 +495,6 @@ export class KubernetesSandboxRunner implements SandboxRunner { workdir: state.workdir ?? DEFAULT_WORKDIR, daemonUrl, daemonForward, - devForward, workload: state.workload ?? null, daemonBootId: health.bootId, }; @@ -561,12 +523,6 @@ export class KubernetesSandboxRunner implements SandboxRunner { return null; } - const devForward = this.previewUrlPattern - ? null - : await this.openForwarder(podName, DEV_CONTAINER_PORT, handle).catch( - () => null, - ); - return { id, handle, @@ -575,7 +531,6 @@ export class KubernetesSandboxRunner implements SandboxRunner { workdir: DEFAULT_WORKDIR, daemonUrl, daemonForward, - devForward, workload: null, daemonBootId: health.bootId, }; @@ -606,13 +561,18 @@ export class KubernetesSandboxRunner implements SandboxRunner { return `${HANDLE_PREFIX}${hashSandboxId(id, HANDLE_HASH_LEN)}`; } - private composePreviewUrl(rec: K8sRecord): string | null { + // Local mode: route preview traffic through the daemon port-forward, not + // a separate dev forwarder. The daemon serves /_decopilot_vm/* + /health + // in-process and reverse-proxies everything else to in-pod localhost:DEV_PORT + // (with CSP/X-Frame stripping + HMR bootstrap injection). Pointing the URL + // straight at the dev port would bypass that proxy and break SSE + iframe + // embedding. Production mode (previewUrlPattern set) goes through the + // ingress-terminated URL the operator emits. + private composePreviewUrl(rec: K8sRecord): string { if (this.previewUrlPattern) { return applyPreviewPattern(this.previewUrlPattern, rec.handle); } - return rec.devForward - ? `http://127.0.0.1:${rec.devForward.localPort}/` - : null; + return `http://127.0.0.1:${rec.daemonForward.localPort}/`; } private toSandbox(rec: K8sRecord): Sandbox { From d7866ca7bb4bc2b74d1e26b6852add67e6f2c38c Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 00:13:54 -0300 Subject: [PATCH 07/16] Implement WebSocket proxying and enhance port discovery in daemon - Introduced a WebSocket proxy to handle upgrades for Vite's HMR and other dev-server WebSocket connections, ensuring seamless communication through the daemon. - Enhanced port discovery logic to dynamically identify listening ports of descendant processes, improving the accuracy of the dev server's operational context. - Refactored the upstream probing mechanism to utilize candidate ports, allowing for more robust detection of the active development server. - Updated the proxy handler to resolve the actual listening port dynamically, ensuring consistent routing of requests. - Added comprehensive comments and documentation to clarify the new functionality and its implications for the daemon's operation. --- packages/sandbox/daemon/entry.ts | 61 +++++- packages/sandbox/daemon/events/sse.ts | 6 +- packages/sandbox/daemon/probe.ts | 108 +++++++++-- .../sandbox/daemon/process/port-discovery.ts | 173 ++++++++++++++++++ .../sandbox/daemon/process/run-process.ts | 17 +- packages/sandbox/daemon/proxy.ts | 8 +- packages/sandbox/daemon/ws-proxy.ts | 94 ++++++++++ 7 files changed, 446 insertions(+), 21 deletions(-) create mode 100644 packages/sandbox/daemon/process/port-discovery.ts create mode 100644 packages/sandbox/daemon/ws-proxy.ts diff --git a/packages/sandbox/daemon/entry.ts b/packages/sandbox/daemon/entry.ts index dae065dec0..336f07fce2 100644 --- a/packages/sandbox/daemon/entry.ts +++ b/packages/sandbox/daemon/entry.ts @@ -18,9 +18,11 @@ import { makeScriptsHandler } from "./routes/scripts"; import { makeHealthHandler } from "./routes/health"; import { makeEventsHandler } from "./routes/events-stream"; import { makeProxyHandler } from "./proxy"; +import { makeWsUpgrader, type WsProxyData } from "./ws-proxy"; import { jsonResponse } from "./routes/body-parser"; import { startUpstreamProbe } from "./probe"; import { BranchStatusMonitor } from "./git/branch-status"; +import { discoverDescendantListeningPorts } from "./process/port-discovery"; // Auto-generate DAEMON_BOOT_ID when not provided (dev/test). In production // the runner supplies a per-container UUID via env. @@ -46,13 +48,46 @@ const orchestrator = new SetupOrchestrator({ const branchStatus = new BranchStatusMonitor(config, broadcaster); let discoveredScripts: string[] | null = null; + +// Build the ordered candidate-port list each tick: +// 1. Ports any descendant of a daemon-managed dev process is listening on +// (Vite v7 / Next / Astro / etc. mostly ignore PORT=$DEV_PORT, so this +// is the source of truth.) +// 2. config.devPort — the env-hint fallback. Honored by frameworks that +// respect PORT, and used by the e2e tests where there's no managed +// dev process and the upstream is started externally. +const excludeFromDiscovery = new Set([config.proxyPort]); +const getCandidatePorts = (): number[] => { + const ordered: number[] = []; + const seen = new Set(); + const push = (p: number) => { + if (!seen.has(p)) { + seen.add(p); + ordered.push(p); + } + }; + const rootPids = processManager.allPids(); + if (rootPids.length > 0) { + for (const port of discoverDescendantListeningPorts({ + rootPids, + excludePorts: excludeFromDiscovery, + })) { + push(port); + } + } + push(config.devPort); + return ordered; +}; + const lastStatus = startUpstreamProbe({ upstreamHost: "localhost", - upstreamPort: config.devPort, + getCandidatePorts, onChange: (s) => broadcaster.broadcastEvent("status", { type: "status", ...s }), }); +const getDevPort = (): number => lastStatus.port ?? config.devPort; + const scriptsHandler = makeScriptsHandler(() => discoveredScripts ?? []); // Intercept the `scripts` event so SSE replay can serve the latest list on @@ -90,16 +125,29 @@ const eventsH = makeEventsHandler({ getActiveProcesses: () => processManager.activeNames(), getLastBranchStatus: () => branchStatus.getLast(), }); -const proxyH = makeProxyHandler({ config, broadcaster }); +const proxyH = makeProxyHandler({ broadcaster, getDevPort }); +const wsProxy = makeWsUpgrader(getDevPort); -Bun.serve({ +Bun.serve({ port: config.proxyPort, hostname: "0.0.0.0", idleTimeout: 0, - async fetch(req) { + async fetch(req, server) { const url = new URL(req.url); const p = url.pathname; + // WebSocket upgrade — Vite HMR + any other dev-server WS. We forward + // to in-pod localhost:devPort so HMR survives the daemon's reverse + // proxy. Daemon-internal SSE (/_decopilot_vm/events) stays HTTP. + if ( + req.headers.get("upgrade")?.toLowerCase() === "websocket" && + !p.startsWith("/_decopilot_vm/") + ) { + const ok = server.upgrade(req, { data: wsProxy.upgradeData(req) }); + if (ok) return undefined as unknown as Response; + return new Response("Upgrade failed", { status: 400 }); + } + if (p === "/health" && req.method === "GET") return healthH(); if (req.method === "GET" && p === "/_decopilot_vm/events") return eventsH(); @@ -135,6 +183,11 @@ Bun.serve({ return proxyH(req); }, + websocket: { + open: wsProxy.open, + message: wsProxy.message, + close: wsProxy.close, + }, }); // Start the branch-status monitor once .git is on disk. Two paths: diff --git a/packages/sandbox/daemon/events/sse.ts b/packages/sandbox/daemon/events/sse.ts index 81e61fc2ea..5c7295140e 100644 --- a/packages/sandbox/daemon/events/sse.ts +++ b/packages/sandbox/daemon/events/sse.ts @@ -3,7 +3,11 @@ import { sseFormat } from "./sse-format"; export interface SseHandshakeDeps { broadcaster: Broadcaster; - getLastStatus: () => { ready: boolean; htmlSupport: boolean }; + getLastStatus: () => { + ready: boolean; + htmlSupport: boolean; + port: number | null; + }; getDiscoveredScripts: () => string[] | null; getActiveProcesses: () => string[]; getLastBranchStatus: () => unknown | null; diff --git a/packages/sandbox/daemon/probe.ts b/packages/sandbox/daemon/probe.ts index c32ab51026..aa4d19fb5b 100644 --- a/packages/sandbox/daemon/probe.ts +++ b/packages/sandbox/daemon/probe.ts @@ -3,34 +3,120 @@ import { FAST_PROBE_LIMIT, FAST_PROBE_MS, SLOW_PROBE_MS } from "./constants"; export interface ProbeState { ready: boolean; htmlSupport: boolean; + /** The port that last responded to HEAD `/`, or null if none yet. */ + port: number | null; } export interface ProbeDeps { upstreamHost: string; - upstreamPort: number; + /** + * Candidate ports to score each tick. All are probed in parallel; the + * one with the highest "looks like the dev preview" score wins. Empty + * array → state stays { ready:false, port:null }. + */ + getCandidatePorts: () => number[]; onChange: (state: ProbeState) => void; } +interface ProbeResult { + port: number; + responded: boolean; + ready: boolean; + htmlSupport: boolean; + /** Higher = more likely to be the actual dev preview surface. */ + score: number; +} + +interface HeadResult { + ok: boolean; + status: number; + isHtml: boolean; +} + +/** + * Score a port. The `/@vite/client` probe disambiguates Vite from any + * other listener that happens to also serve HTML at `/`: Vite returns + * JS, anything else returns HTML or 404. Sidecar runtimes (workerd, + * esbuild) are filtered upstream by port-discovery.ts — we don't probe + * them from here. + */ +function score(root: HeadResult | null, viteClient: HeadResult | null): number { + let s = 0; + if (root) { + if (root.ok) s += root.isHtml ? 100 : 50; + else s += 10; // HTTP, but not 2xx-3xx + } + if (viteClient && viteClient.ok && !viteClient.isHtml) s += 50; + return s; +} + /** Kicks off the probe loop; returns the current state (live-updated). */ export function startUpstreamProbe(deps: ProbeDeps): ProbeState { - const state: ProbeState = { ready: false, htmlSupport: false }; + const state: ProbeState = { ready: false, htmlSupport: false, port: null }; let count = 0; - const tick = async () => { - const prev = state.ready; + const head = async (url: string): Promise => { try { - const res = await fetch( - `http://${deps.upstreamHost}:${deps.upstreamPort}/`, - { method: "HEAD", signal: AbortSignal.timeout(5000) }, - ); + const res = await fetch(url, { + method: "HEAD", + signal: AbortSignal.timeout(5000), + }); const ct = (res.headers.get("content-type") ?? "").toLowerCase(); - state.ready = res.status >= 200 && res.status < 400; - state.htmlSupport = ct.includes("text/html"); + return { + ok: res.status >= 200 && res.status < 400, + status: res.status, + isHtml: ct.includes("text/html"), + }; } catch { + return null; + } + }; + + const tryOne = async (port: number): Promise => { + const base = `http://${deps.upstreamHost}:${port}`; + // Probe `/` first; only ask `/@vite/client` if root looks like a real + // HTML responder. Avoids hammering ports that don't speak HTTP. + const root = await head(`${base}/`); + let viteClient: HeadResult | null = null; + if (root && root.ok && root.isHtml) { + viteClient = await head(`${base}/@vite/client`); + } + return { + port, + responded: root !== null, + ready: root?.ok ?? false, + htmlSupport: root?.isHtml ?? false, + score: score(root, viteClient), + }; + }; + + const tick = async () => { + const prevReady = state.ready; + const prevPort = state.port; + const prevHtml = state.htmlSupport; + const candidates = deps.getCandidatePorts(); + const results = await Promise.all(candidates.map(tryOne)); + // Highest score wins; on tie, the candidate-list order (already + // discovered-first) breaks it — `Array.sort` is stable in modern JS. + const best = results + .filter((r) => r.responded) + .sort((a, b) => b.score - a.score)[0]; + if (best) { + state.port = best.port; + state.ready = best.ready; + state.htmlSupport = best.htmlSupport; + } else { + state.port = null; state.ready = false; state.htmlSupport = false; } - if (state.ready !== prev) deps.onChange({ ...state }); + if ( + prevReady !== state.ready || + prevPort !== state.port || + prevHtml !== state.htmlSupport + ) { + deps.onChange({ ...state }); + } count++; setTimeout(tick, count < FAST_PROBE_LIMIT ? FAST_PROBE_MS : SLOW_PROBE_MS); }; diff --git a/packages/sandbox/daemon/process/port-discovery.ts b/packages/sandbox/daemon/process/port-discovery.ts new file mode 100644 index 0000000000..8b8cd107da --- /dev/null +++ b/packages/sandbox/daemon/process/port-discovery.ts @@ -0,0 +1,173 @@ +import { readdirSync, readFileSync, readlinkSync } from "node:fs"; + +/** + * Discover TCP ports the descendants of a given pid are listening on. + * + * The daemon launches `bun run dev` (etc.) with `PORT=$DEV_PORT` as a hint, + * but most modern dev servers (Vite v7, Next, Astro …) ignore that env and + * pick their own port. Reading /proc lets the proxy follow whatever the + * dev process actually bound to. + * + * Linux-only; on macOS/test hosts the readSync calls throw and we fall back + * to an empty result. Callers should treat "no discovery" as "use the env + * hint" — see entry.ts for the candidate-list composition. + */ + +const SOCKET_INODE_RE = /^socket:\[(\d+)\]$/; + +/** + * Sidecar runtimes spawned by dev servers that listen on TCP but are NOT + * the user-facing preview surface. Probing them with HEAD requests can + * crash their handlers (workerd throws on any request whose worker code + * does relative `fetch()`; node --inspect treats it as a debugger probe) + * and pollutes the dev process's own logs. Filtered out of port + * discovery by checking /proc//comm. + */ +const SIDECAR_COMMS = new Set([ + "workerd", + "esbuild", + "wrangler", + "tsserver", +]); + +/** Reads `/proc//comm` (truncated process name); empty string on error. */ +function getProcessComm(pid: number): string { + try { + return readFileSync(`/proc/${pid}/comm`, "utf8").trim(); + } catch { + return ""; + } +} + +/** Walks /proc/*\/stat to compute the transitive children of `rootPid`. */ +function getDescendantPids(rootPid: number): number[] { + let entries: string[]; + try { + entries = readdirSync("/proc"); + } catch { + return []; + } + const ppids = new Map(); + for (const e of entries) { + const pid = Number(e); + if (!Number.isInteger(pid) || pid <= 0) continue; + try { + const stat = readFileSync(`/proc/${pid}/stat`, "utf8"); + // Format: pid (comm) state ppid … comm is parenthesised and may + // contain spaces or unbalanced inner parens — split off everything + // up to the LAST `)` to skip it safely. + const close = stat.lastIndexOf(")"); + if (close === -1) continue; + const tail = stat.slice(close + 2).split(" "); + const ppid = Number(tail[1]); + if (Number.isInteger(ppid)) ppids.set(pid, ppid); + } catch { + // pid exited between readdir and read — skip + } + } + const out = new Set([rootPid]); + let changed = true; + while (changed) { + changed = false; + for (const [pid, ppid] of ppids) { + if (out.has(ppid) && !out.has(pid)) { + out.add(pid); + changed = true; + } + } + } + out.delete(rootPid); + return Array.from(out); +} + +/** Resolves the socket inodes a pid currently has open via /proc//fd. */ +function getProcessSocketInodes(pid: number): Set { + const inodes = new Set(); + let fds: string[]; + try { + fds = readdirSync(`/proc/${pid}/fd`); + } catch { + return inodes; + } + for (const fd of fds) { + try { + const link = readlinkSync(`/proc/${pid}/fd/${fd}`); + const m = SOCKET_INODE_RE.exec(link); + if (m) inodes.add(Number(m[1])); + } catch { + // fd may have closed mid-scan — skip + } + } + return inodes; +} + +interface ListeningRow { + port: number; + inode: number; +} + +/** Parses the LISTEN rows (state 0A) from /proc/net/tcp + tcp6. */ +function readListeningTcp(): ListeningRow[] { + const out: ListeningRow[] = []; + for (const path of ["/proc/net/tcp", "/proc/net/tcp6"]) { + let raw: string; + try { + raw = readFileSync(path, "utf8"); + } catch { + continue; + } + const lines = raw.split("\n"); + // Columns: sl local rem state tx_queue rx_queue tr tm->when + // retrnsmt uid timeout inode … + for (let i = 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) continue; + const cols = line.split(/\s+/); + if (cols.length < 10) continue; + if (cols[3] !== "0A") continue; + const local = cols[1]; + const portHex = local.split(":")[1]; + if (!portHex) continue; + const port = parseInt(portHex, 16); + if (!Number.isInteger(port) || port <= 0) continue; + const inode = Number(cols[9]); + if (!Number.isInteger(inode)) continue; + out.push({ port, inode }); + } + } + return out; +} + +export interface DiscoverPortsOpts { + rootPids: readonly number[]; + excludePorts?: ReadonlySet; +} + +/** + * Returns the listening TCP ports owned by any descendant of `rootPids`, + * minus `excludePorts`. Empty array on non-Linux or any read failure. + */ +export function discoverDescendantListeningPorts({ + rootPids, + excludePorts, +}: DiscoverPortsOpts): number[] { + if (rootPids.length === 0) return []; + const owned = new Set(); + for (const root of rootPids) { + for (const pid of [root, ...getDescendantPids(root)]) { + // Skip sidecars (workerd / esbuild / etc.) — their listening sockets + // are runtime internals, not preview surfaces, and probing them can + // wedge the dev server. + if (SIDECAR_COMMS.has(getProcessComm(pid))) continue; + for (const inode of getProcessSocketInodes(pid)) owned.add(inode); + } + } + if (owned.size === 0) return []; + const ports = new Set(); + for (const row of readListeningTcp()) { + if (!owned.has(row.inode)) continue; + if (excludePorts?.has(row.port)) continue; + ports.add(row.port); + } + return Array.from(ports); +} diff --git a/packages/sandbox/daemon/process/run-process.ts b/packages/sandbox/daemon/process/run-process.ts index 37c6f52951..217972e7d6 100644 --- a/packages/sandbox/daemon/process/run-process.ts +++ b/packages/sandbox/daemon/process/run-process.ts @@ -16,6 +16,15 @@ export class ProcessManager { return Array.from(this.children.keys()); } + /** Pids of every child currently tracked — used to scope port discovery. */ + allPids(): number[] { + const out: number[] = []; + for (const child of this.children.values()) { + if (typeof child.pid === "number") out.push(child.pid); + } + return out; + } + run(source: string, cmd: string, label: string): ChildProcess { const existing = this.children.get(source); if (existing) { @@ -29,8 +38,14 @@ export class ProcessManager { this.children.delete(source); } this.deps.broadcaster.broadcastChunk(source, `${label}\r\n`); + // stdin is `pipe` (not `ignore`) so it's an open writable that never + // closes. Vite's CLI shortcuts call setRawMode then watch stdin for EOF; + // with stdin closed at spawn the child sees EOF immediately and exits + // right after announcing it's ready. Keeping the pipe open without ever + // writing to it is the cheapest way to keep long-running dev servers + // alive under the `script` PTY wrapper. const opts: Parameters[2] = { - stdio: ["ignore", "pipe", "pipe"], + stdio: ["pipe", "pipe", "pipe"], env: this.deps.env, }; if (this.deps.dropPrivileges) { diff --git a/packages/sandbox/daemon/proxy.ts b/packages/sandbox/daemon/proxy.ts index 16f1f0e752..5013b12f17 100644 --- a/packages/sandbox/daemon/proxy.ts +++ b/packages/sandbox/daemon/proxy.ts @@ -1,13 +1,13 @@ import { BOOTSTRAP_SCRIPT } from "./constants"; import type { Broadcaster } from "./events/broadcast"; -import type { Config } from "./types"; export interface ProxyDeps { - config: Config; broadcaster: Broadcaster; + /** Resolved each request — follows the dev process's actual listening port. */ + getDevPort: () => number; } -export function makeProxyHandler({ config, broadcaster }: ProxyDeps) { +export function makeProxyHandler({ broadcaster, getDevPort }: ProxyDeps) { function log(...args: string[]) { const msg = `[daemon] ${new Date().toISOString()} ${args.join(" ")}`; broadcaster.broadcastChunk("daemon", msg + "\r\n"); @@ -16,7 +16,7 @@ export function makeProxyHandler({ config, broadcaster }: ProxyDeps) { return async (req: Request): Promise => { const url = new URL(req.url); log("proxy", req.method, url.pathname); - const target = `http://localhost:${config.devPort}${url.pathname}${url.search}`; + const target = `http://localhost:${getDevPort()}${url.pathname}${url.search}`; const outHeaders = new Headers(req.headers); outHeaders.delete("accept-encoding"); outHeaders.delete("host"); diff --git a/packages/sandbox/daemon/ws-proxy.ts b/packages/sandbox/daemon/ws-proxy.ts new file mode 100644 index 0000000000..b53fe9bce6 --- /dev/null +++ b/packages/sandbox/daemon/ws-proxy.ts @@ -0,0 +1,94 @@ +/** + * Transparent WebSocket reverse proxy for the daemon. + * + * The daemon's HTTP proxy uses fetch(), which doesn't carry WebSocket + * upgrade semantics. Without this, Vite's HMR client (and any other + * dev-server WS) gets 502 on the upgrade, retries a few times, then + * triggers a full-page reload as recovery — the user sees the page load + * then immediately reload, in a loop. + * + * On upgrade we stash the rewritten in-pod target URL (plus the client's + * negotiated subprotocols) in ws.data, then open the upstream WS on the + * `open` callback and bridge frames in both directions. Subprotocols + * (`vite-hmr`, `vite-ping`, …) are forwarded — Vite ignores connections + * that drop them. + */ +import type { ServerWebSocket } from "bun"; + +export interface WsProxyData { + /** Full upstream URL — `ws://localhost:?`. */ + target: string; + /** Subprotocols the client advertised on the upgrade request. */ + protocols: string[] | undefined; + upstream: WebSocket | null; + /** Frames received from the client before the upstream handshake completes. */ + pending: (string | ArrayBuffer | Uint8Array)[]; +} + +export function makeWsUpgrader(getDevPort: () => number) { + return { + /** Build the per-connection state attached to ws.data at upgrade time. */ + upgradeData(req: Request): WsProxyData { + const url = new URL(req.url); + const target = `ws://localhost:${getDevPort()}${url.pathname}${url.search}`; + const protoHeader = req.headers.get("sec-websocket-protocol"); + const protocols = protoHeader + ? protoHeader + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + : undefined; + return { target, protocols, upstream: null, pending: [] }; + }, + + open(ws: ServerWebSocket): void { + const upstream = new WebSocket(ws.data.target, ws.data.protocols); + upstream.binaryType = "arraybuffer"; + ws.data.upstream = upstream; + + upstream.addEventListener("open", () => { + for (const frame of ws.data.pending) { + try { + upstream.send(frame as never); + } catch {} + } + ws.data.pending.length = 0; + }); + upstream.addEventListener("message", (e) => { + try { + ws.send(e.data as never); + } catch {} + }); + upstream.addEventListener("close", () => { + try { + ws.close(); + } catch {} + }); + upstream.addEventListener("error", () => { + try { + ws.close(); + } catch {} + }); + }, + + message(ws: ServerWebSocket, message: string | Buffer): void { + const upstream = ws.data.upstream; + const frame = typeof message === "string" ? message : message.buffer; + if (upstream && upstream.readyState === WebSocket.OPEN) { + try { + upstream.send(frame as never); + } catch {} + } else { + ws.data.pending.push(frame as ArrayBuffer | string); + } + }, + + close(ws: ServerWebSocket): void { + try { + ws.data.upstream?.close(); + } catch {} + }, + }; +} + +export type WsUpgrader = ReturnType; From ad50a78046dda0e9e252a363d85cdfb23582a698 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 00:47:07 -0300 Subject: [PATCH 08/16] Enhance sandbox configuration in Helm charts - Added support for nodeSelector and tolerations in the sandbox values, allowing for better pod scheduling and resource management. - Introduced hostUsers option to enable user namespace remapping, enhancing security by preventing container escapes to real node UIDs. - Implemented readOnlyRootFilesystem configuration to improve security and stability, with provisions for necessary volume mounts. - Updated agent-sandbox manifest to include PodSecurity admission labels, enforcing baseline security policies for the namespace. These changes aim to improve the security and configurability of sandbox deployments. --- .../templates/agent-sandbox-manifest.yaml | 15 ++++++ deploy/helm/templates/sandbox-template.yaml | 52 ++++++++++++++++++- deploy/helm/values.yaml | 28 ++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/deploy/helm/charts/agent-sandbox/templates/agent-sandbox-manifest.yaml b/deploy/helm/charts/agent-sandbox/templates/agent-sandbox-manifest.yaml index 5e65a6a87c..453305dc58 100644 --- a/deploy/helm/charts/agent-sandbox/templates/agent-sandbox-manifest.yaml +++ b/deploy/helm/charts/agent-sandbox/templates/agent-sandbox-manifest.yaml @@ -1,10 +1,25 @@ # Vendored from kubernetes-sigs/agent-sandbox v0.4.2 via vendor.sh. # Do not edit by hand — re-run vendor.sh to refresh. # Contains: controller Deployments, RBAC, Namespace, Service, ServiceAccount. +# +# LOCAL EDIT — preserve when re-running vendor.sh: +# PodSecurity admission labels added to the Namespace below. `baseline` +# is enforced (operator controller pod runs without an explicit +# securityContext; `restricted` would block it until that's patched). +# `restricted` is set as warn/audit so violations from sandbox pods or +# the controller surface in audit logs without rejecting admission. +# When the operator's pod spec hardens to `restricted`, flip enforce. kind: Namespace apiVersion: v1 metadata: name: agent-sandbox-system + labels: + pod-security.kubernetes.io/enforce: baseline + pod-security.kubernetes.io/enforce-version: latest + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest + pod-security.kubernetes.io/audit: restricted + pod-security.kubernetes.io/audit-version: latest --- diff --git a/deploy/helm/templates/sandbox-template.yaml b/deploy/helm/templates/sandbox-template.yaml index 38651d131a..4adaf4694b 100644 --- a/deploy/helm/templates/sandbox-template.yaml +++ b/deploy/helm/templates/sandbox-template.yaml @@ -26,6 +26,21 @@ spec: app.kubernetes.io/name: mesh-sandbox spec: automountServiceAccountToken: false + {{- with .Values.sandbox.kubernetes.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.sandbox.kubernetes.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if not .Values.sandbox.kubernetes.hostUsers }} + # User namespace remap: UID 1000 inside the pod maps to a high + # subordinate UID on the node, so a container escape lands as a + # nobody-user, not as a real node UID. Requires K8s 1.30+ and a + # containerd/kernel that support userns (EKS default AMIs are fine). + hostUsers: false + {{- end }} securityContext: runAsNonRoot: true runAsUser: 1000 @@ -44,6 +59,18 @@ spec: - name: WORKDIR value: "/app" # DAEMON_TOKEN is injected per-claim via SandboxClaim.spec.env. + {{- if .Values.sandbox.kubernetes.readOnlyRootFilesystem }} + # With RO rootfs + emptyDir on /app, the mount root is owned + # root:1000 (fsGroup). Git 2.35+'s "dubious ownership" check + # would refuse to operate. Disable the check inside the + # sandbox — single-tenant pod, no untrusted same-pod user. + - name: GIT_CONFIG_COUNT + value: "1" + - name: GIT_CONFIG_KEY_0 + value: "safe.directory" + - name: GIT_CONFIG_VALUE_0 + value: "*" + {{- end }} ports: - name: daemon containerPort: 9000 @@ -57,5 +84,28 @@ spec: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] - readOnlyRootFilesystem: false + readOnlyRootFilesystem: {{ .Values.sandbox.kubernetes.readOnlyRootFilesystem }} + {{- if .Values.sandbox.kubernetes.readOnlyRootFilesystem }} + volumeMounts: + - name: workdir + mountPath: /app + - name: tmp + mountPath: /tmp + - name: home + mountPath: /home/sandbox + {{- end }} + {{- if .Values.sandbox.kubernetes.readOnlyRootFilesystem }} + volumes: + # Sized to match the per-container ephemeral-storage limit shape; + # individual mounts get a slice. Adjust if a workload needs more. + - name: workdir + emptyDir: + sizeLimit: 5Gi + - name: tmp + emptyDir: + sizeLimit: 1Gi + - name: home + emptyDir: + sizeLimit: 2Gi + {{- end }} {{- end }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 96d6cb9a42..c7655b44ac 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -352,6 +352,34 @@ sandbox: # that terminate preview URLs. Leave empty to skip the rule (sandbox # previews are then unreachable from outside the cluster). previewGatewayNamespace: "" + # Pin sandbox pods to a dedicated NodePool so a container escape lands + # on a node that has no mesh / postgres / NATS / OTel pods on it. Pair + # with a Karpenter NodePool that taints + labels matching nodes; see + # deploy/helm/README.md for the snippet. Empty defaults run sandbox + # pods on whatever the scheduler picks (current behavior). + nodeSelector: {} + # nodeSelector: + # workload: sandbox + tolerations: [] + # tolerations: + # - key: workload + # operator: Equal + # value: sandbox + # effect: NoSchedule + # User namespace remap (`spec.hostUsers: false`): UID 1000 inside the + # pod maps to a high, unprivileged subordinate UID on the node, so a + # container escape doesn't land as a real node UID. Requires K8s + # 1.30+ with a containerd/kernel that support userns (EKS default + # AMIs from late 2024 onward are fine). Defaults to current behavior + # (host users); flip to false to opt in. + hostUsers: true + # Read-only root filesystem. When true, /app + /tmp + /home/sandbox + # are remounted as emptyDirs and `safe.directory '*'` is set so git + # works against the chowned mount. Validate end-to-end (clone + + # bun/npm install + dev server start) on staging before flipping; the + # daemon may write to paths we haven't covered. Defaults to false to + # preserve current behavior on upgrade. + readOnlyRootFilesystem: false warmPool: # Enable only after measuring cold-start pain; every warm pod costs # the full resources.requests above. From c498543b653bebab9110522405ba53390c629b79 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 00:50:54 -0300 Subject: [PATCH 09/16] Enhance sandbox runner configuration for Kubernetes - Introduced a new environment variable, MESH_SANDBOX_PREVIEW_URL_PATTERN, to allow the specification of a public URL pattern for sandbox previews. - Updated Docker and Kubernetes sandbox runners to utilize the preview URL pattern, improving accessibility for users. - Modified Helm chart values to include the new preview URL pattern configuration, ensuring proper deployment in production environments. These changes aim to enhance the configurability and accessibility of sandbox previews in Kubernetes deployments. --- .github/workflows/release-mesh-sandbox.yaml | 78 +++++++++++++++++++++ apps/mesh/src/sandbox/lifecycle.ts | 13 +++- deploy/helm/templates/configmap.yaml | 8 +++ deploy/helm/templates/sandbox-rbac.yaml | 50 +++++++++++++ deploy/helm/values.yaml | 8 +++ 5 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release-mesh-sandbox.yaml create mode 100644 deploy/helm/templates/sandbox-rbac.yaml diff --git a/.github/workflows/release-mesh-sandbox.yaml b/.github/workflows/release-mesh-sandbox.yaml new file mode 100644 index 0000000000..750f0e58be --- /dev/null +++ b/.github/workflows/release-mesh-sandbox.yaml @@ -0,0 +1,78 @@ +name: Release Mesh Sandbox Image + +on: + push: + branches: [main] + paths: + - "packages/sandbox/**" + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/mesh-sandbox + +jobs: + build-push: + name: Build & Push mesh-sandbox image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.5" + + - name: Install dependencies + run: bun install + + # The Dockerfile copies daemon/dist/daemon.js into the image, so the + # bundle has to exist before `docker build` runs. + - name: Build daemon bundle + run: bun run --cwd=packages/sandbox build + + - name: Read sandbox version + id: version + run: | + VERSION=$(bun -e "console.log(require('./packages/sandbox/package.json').version)") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ steps.version.outputs.version }} + type=raw,value=latest + type=sha,format=short + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./packages/sandbox + file: ./packages/sandbox/image/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 diff --git a/apps/mesh/src/sandbox/lifecycle.ts b/apps/mesh/src/sandbox/lifecycle.ts index 29ffe7c0b9..38b3312604 100644 --- a/apps/mesh/src/sandbox/lifecycle.ts +++ b/apps/mesh/src/sandbox/lifecycle.ts @@ -18,14 +18,23 @@ import { KyselySandboxRunnerStateStore } from "@/storage/sandbox-runner-state"; const runners: Partial> = {}; +// Set in prod (k8s/docker behind ingress) so the runner skips the local +// 127.0.0.1 port-forward path and emits a URL the user's browser can +// actually reach. Empty/unset = local forwarder fallback (dev). +function readPreviewUrlPattern(): string | undefined { + const raw = process.env.MESH_SANDBOX_PREVIEW_URL_PATTERN; + return raw && raw.trim() !== "" ? raw : undefined; +} + async function instantiate( kind: RunnerKind, ctx: MeshContext, ): Promise { const stateStore = new KyselySandboxRunnerStateStore(ctx.db); + const previewUrlPattern = readPreviewUrlPattern(); switch (kind) { case "docker": - return new DockerSandboxRunner({ stateStore }); + return new DockerSandboxRunner({ stateStore, previewUrlPattern }); case "freestyle": { // Dynamic import — freestyle SDK is an optionalDependency so // docker-only deploys don't need it installed. @@ -41,7 +50,7 @@ async function instantiate( const { KubernetesSandboxRunner } = await import( "@decocms/sandbox/runner/k8s" ); - return new KubernetesSandboxRunner({ stateStore }); + return new KubernetesSandboxRunner({ stateStore, previewUrlPattern }); } default: { const exhaustive: never = kind; diff --git a/deploy/helm/templates/configmap.yaml b/deploy/helm/templates/configmap.yaml index c66f1d00e7..e655507e17 100644 --- a/deploy/helm/templates/configmap.yaml +++ b/deploy/helm/templates/configmap.yaml @@ -17,3 +17,11 @@ data: {{- if ne (lower (default "sqlite" .Values.database.engine)) "postgresql" }} DATABASE_URL: {{ include "chart-deco-studio.databaseUrl" . | trim | quote }} {{- end }} + {{- if .Values.sandbox.kubernetes.enabled }} + {{- if not (hasKey .Values.configMap.meshConfig "MESH_SANDBOX_RUNNER") }} + MESH_SANDBOX_RUNNER: "kubernetes" + {{- end }} + {{- with .Values.sandbox.kubernetes.previewUrlPattern }} + MESH_SANDBOX_PREVIEW_URL_PATTERN: {{ . | quote }} + {{- end }} + {{- end }} diff --git a/deploy/helm/templates/sandbox-rbac.yaml b/deploy/helm/templates/sandbox-rbac.yaml new file mode 100644 index 0000000000..ba8227d1bb --- /dev/null +++ b/deploy/helm/templates/sandbox-rbac.yaml @@ -0,0 +1,50 @@ +{{- if .Values.sandbox.kubernetes.enabled }} +# RBAC for the mesh ServiceAccount to drive the agent-sandbox operator from +# inside the cluster. The runner (packages/sandbox/server/runner/k8s/) needs: +# - sandboxclaims CRUD/patch (per-tenant claim lifecycle, idle TTL refresh) +# - sandboxes get/list/watch (waitForSandboxReady streams `?watch=true`) +# - pods/portforward create (kubectl-style tunnel to the daemon container) +# Pods get is included for portforward error paths; the runner itself doesn't +# read pod specs directly. +# +# Scope: Role in agent-sandbox-system (the operator's namespace). The mesh +# ServiceAccount lives in {{ .Release.Namespace }} and the RoleBinding crosses +# namespaces by referencing it explicitly. Keeps blast radius of a mesh +# compromise limited to the sandbox namespace. +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: mesh-sandbox-runner + namespace: agent-sandbox-system + labels: + {{- include "chart-deco-studio.labels" . | nindent 4 }} +rules: + - apiGroups: ["extensions.agents.x-k8s.io"] + resources: ["sandboxclaims"] + verbs: ["get", "create", "delete", "patch"] + - apiGroups: ["agents.x-k8s.io"] + resources: ["sandboxes"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get"] + - apiGroups: [""] + resources: ["pods/portforward"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: mesh-sandbox-runner + namespace: agent-sandbox-system + labels: + {{- include "chart-deco-studio.labels" . | nindent 4 }} +subjects: + - kind: ServiceAccount + name: {{ include "chart-deco-studio.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +roleRef: + kind: Role + name: mesh-sandbox-runner + apiGroup: rbac.authorization.k8s.io +{{- end }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index c7655b44ac..d59646a7b0 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -352,6 +352,14 @@ sandbox: # that terminate preview URLs. Leave empty to skip the rule (sandbox # previews are then unreachable from outside the cluster). previewGatewayNamespace: "" + # Public URL pattern that ingress terminates on for sandbox previews. + # The runner substitutes {handle} (or whatever applyPreviewPattern + # supports) with the per-claim handle. Set in prod once a wildcard + # gateway + cert is in place; leave empty in local dev so the runner + # falls back to the 127.0.0.1 port-forward URL. Surfaced into the + # mesh container as MESH_SANDBOX_PREVIEW_URL_PATTERN. + previewUrlPattern: "" + # previewUrlPattern: "https://{handle}.preview.example.com" # Pin sandbox pods to a dedicated NodePool so a container escape lands # on a node that has no mesh / postgres / NATS / OTel pods on it. Pair # with a Karpenter NodePool that taints + labels matching nodes; see From b07193e507d468808a74622b931390aaf1aa677b Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 10:07:03 -0300 Subject: [PATCH 10/16] Implement sandbox preview proxy and enhance WebSocket handling - Introduced a sandbox preview reverse-proxy to route requests for `.preview.` to the corresponding sandbox daemon, improving accessibility for preview environments. - Enhanced WebSocket handling to support upgrades and message processing for preview connections, ensuring seamless communication for development workflows. - Updated the Helm chart to include configurations for the new preview URL pattern and related settings, facilitating deployment in Kubernetes environments. These changes aim to improve the functionality and usability of sandbox previews in the development process. --- apps/mesh/src/index.ts | 65 ++++ apps/mesh/src/sandbox/lifecycle.ts | 26 +- apps/mesh/src/sandbox/preview-proxy.test.ts | 250 ++++++++++++++ apps/mesh/src/sandbox/preview-proxy.ts | 317 ++++++++++++++++++ apps/mesh/src/tools/vm/start.ts | 1 + deploy/helm/README.md | 103 +++++- deploy/helm/examples/postgres-kind.yaml | 81 +++++ deploy/helm/examples/values-kind.yaml | 152 +++++++++ deploy/helm/templates/pvc.yaml | 6 +- .../templates/sandbox-network-policy.yaml | 13 +- .../helm/templates/sandbox-preview-cert.yaml | 34 ++ .../templates/sandbox-preview-gateway.yaml | 78 +++++ deploy/helm/templates/sandbox-rbac.yaml | 8 +- deploy/helm/templates/sandbox-template.yaml | 15 + deploy/helm/values.yaml | 56 +++- deploy/k8s-sandbox/local/README.md | 76 ++++- deploy/k8s-sandbox/local/down.sh | 17 + .../dashboards/sandbox-overview.json | 115 +++++++ .../values-kube-prometheus-stack.yaml | 59 ++++ .../monitoring/values-otel-collector.yaml | 157 +++++++++ .../k8s-sandbox/local/sandbox-template.yaml | 10 + deploy/k8s-sandbox/local/up.sh | 55 +++ packages/sandbox/server/runner/k8s/client.ts | 9 + packages/sandbox/server/runner/k8s/index.ts | 2 +- packages/sandbox/server/runner/k8s/runner.ts | 199 ++++++++++- packages/sandbox/server/runner/types.ts | 11 + 26 files changed, 1887 insertions(+), 28 deletions(-) create mode 100644 apps/mesh/src/sandbox/preview-proxy.test.ts create mode 100644 apps/mesh/src/sandbox/preview-proxy.ts create mode 100644 deploy/helm/examples/postgres-kind.yaml create mode 100644 deploy/helm/examples/values-kind.yaml create mode 100644 deploy/helm/templates/sandbox-preview-cert.yaml create mode 100644 deploy/helm/templates/sandbox-preview-gateway.yaml create mode 100644 deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json create mode 100644 deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml create mode 100644 deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml diff --git a/apps/mesh/src/index.ts b/apps/mesh/src/index.ts index 5dbbb47081..014b6e9519 100644 --- a/apps/mesh/src/index.ts +++ b/apps/mesh/src/index.ts @@ -78,6 +78,35 @@ function withSecurityHeaders(res: Response): Response { // Closed early in gracefulShutdown so the port frees before the Hono drain. let ingressServers: import("node:net").Server[] = []; +// Sandbox preview reverse-proxy (K8s only). The base domain is parsed at +// boot from MESH_SANDBOX_PREVIEW_URL_PATTERN; null disables the proxy and +// preview-host requests fall through to the normal mesh routing (which 404s +// because nothing matches). The Bun-level WS handler is registered +// unconditionally — when previewBaseDomain is null, no upgrade path runs it. +const { + parsePreviewBaseDomain, + tryHandlePreviewHttp, + tryUpgradePreviewWs, + previewWebSocketHandler, + isPreviewWsData, +} = await import("./sandbox/preview-proxy"); +const { getOrInitSharedRunner: getOrInitRunnerForPreview } = await import( + "./sandbox/lifecycle" +); +const previewBaseDomain = parsePreviewBaseDomain( + process.env.MESH_SANDBOX_PREVIEW_URL_PATTERN, +); +const previewProxyDeps = { + baseDomain: previewBaseDomain ?? "", + getRunner: async () => { + const runner = await getOrInitRunnerForPreview(); + if (!runner || runner.kind !== "kubernetes") return null; + // The K8s runner is the only one that exposes proxyPreviewRequest / + // resolvePreviewUpstreamUrl; cast is safe after the kind check. + return runner as unknown as import("@decocms/sandbox/runner/k8s").KubernetesSandboxRunner; + }, +}; + // Docker-only boot/dev wiring. Both hooks (boot sweep + local ingress) are // intimate with Docker-specific primitives (labels, host-port mappings); // other runners manage their own VM/ingress lifecycle. @@ -140,12 +169,48 @@ const server = Bun.serve({ hostname: "0.0.0.0", // Listen on all network interfaces (required for K8s) reusePort, fetch: async (request, server) => { + // Sandbox preview proxy: matched by Host header. Runs *before* assets + // and the Hono app so a `.preview.` request never hits + // mesh's static-file handler (which would 404 on the dev server's + // bundle paths). WS upgrades short-circuit Bun.serve's fetch by + // returning undefined; HTTP returns a Response. + if (previewBaseDomain) { + // Bun's Server type defaults T=undefined for upgrade(); cast widens + // to our PreviewWsData carrier so the WS handler can stash it. Bun + // doesn't enforce data-type consistency at runtime, only via generics. + const upgradeRes = await tryUpgradePreviewWs( + request, + server as unknown as Parameters[1], + previewProxyDeps, + ); + if (upgradeRes === undefined) return; // upgraded + if (upgradeRes) return upgradeRes; // pre-upgrade error + const httpRes = await tryHandlePreviewHttp(request, previewProxyDeps); + if (httpRes) return httpRes; + } + // Try assets first (static files or dev proxy), then API // Pass server as env so Hono's getConnInfo can access requestIP const assetRes = await handleAssets(request); if (assetRes) return withSecurityHeaders(assetRes); return app.fetch(request, { server }); }, + // Multiplexed WebSocket handler. `ws.data.kind` discriminates which + // upgrader stashed the payload — preview is the only producer today; new + // upgraders should add a tagged `kind` and a branch here. + websocket: { + open(ws) { + if (isPreviewWsData(ws.data)) previewWebSocketHandler.open(ws); + }, + message(ws, message) { + if (isPreviewWsData(ws.data)) { + previewWebSocketHandler.message(ws, message); + } + }, + close(ws) { + if (isPreviewWsData(ws.data)) previewWebSocketHandler.close(ws); + }, + }, development: settings.nodeEnv !== "production", }); diff --git a/apps/mesh/src/sandbox/lifecycle.ts b/apps/mesh/src/sandbox/lifecycle.ts index 38b3312604..09efd701f6 100644 --- a/apps/mesh/src/sandbox/lifecycle.ts +++ b/apps/mesh/src/sandbox/lifecycle.ts @@ -14,6 +14,9 @@ import { type RunnerKind, type SandboxRunner, } from "@decocms/sandbox/runner"; +import { getDb } from "@/database"; +import type { Kysely } from "kysely"; +import type { Database as DatabaseSchema } from "@/storage/types"; import { KyselySandboxRunnerStateStore } from "@/storage/sandbox-runner-state"; const runners: Partial> = {}; @@ -28,9 +31,9 @@ function readPreviewUrlPattern(): string | undefined { async function instantiate( kind: RunnerKind, - ctx: MeshContext, + db: Kysely, ): Promise { - const stateStore = new KyselySandboxRunnerStateStore(ctx.db); + const stateStore = new KyselySandboxRunnerStateStore(db); const previewUrlPattern = readPreviewUrlPattern(); switch (kind) { case "docker": @@ -70,7 +73,24 @@ export async function getRunnerByKind( ): Promise { const cached = runners[kind]; if (cached) return cached; - const runner = await instantiate(kind, ctx); + const runner = await instantiate(kind, ctx.db); + runners[kind] = runner; + return runner; +} + +/** + * Eager runner accessor for paths that need the runner before any user + * request — preview-host proxying at the Bun.serve layer is the only caller + * today. Reads the runner kind from env and constructs without a + * MeshContext (the state store only needs a Kysely instance). Returns null + * when no runner kind is configured. + */ +export async function getOrInitSharedRunner(): Promise { + const kind = tryResolveRunnerKindFromEnv(); + if (!kind) return null; + const cached = runners[kind]; + if (cached) return cached; + const runner = await instantiate(kind, getDb().db); runners[kind] = runner; return runner; } diff --git a/apps/mesh/src/sandbox/preview-proxy.test.ts b/apps/mesh/src/sandbox/preview-proxy.test.ts new file mode 100644 index 0000000000..59ed698b20 --- /dev/null +++ b/apps/mesh/src/sandbox/preview-proxy.test.ts @@ -0,0 +1,250 @@ +import { describe, expect, it } from "bun:test"; +import { + extractHandleFromHost, + parsePreviewBaseDomain, + tryHandlePreviewHttp, + tryUpgradePreviewWs, +} from "./preview-proxy"; + +describe("parsePreviewBaseDomain", () => { + it("extracts the base from {handle}-templated patterns", () => { + expect(parsePreviewBaseDomain("https://{handle}.preview.decocms.com")).toBe( + "preview.decocms.com", + ); + }); + + it("extracts from the bare-pattern form (no template)", () => { + expect(parsePreviewBaseDomain("https://preview.example.com")).toBe( + "preview.example.com", + ); + }); + + it("returns null for empty/unset patterns", () => { + expect(parsePreviewBaseDomain(null)).toBeNull(); + expect(parsePreviewBaseDomain(undefined)).toBeNull(); + expect(parsePreviewBaseDomain("")).toBeNull(); + expect(parsePreviewBaseDomain(" ")).toBeNull(); + }); + + it("returns null for malformed URLs", () => { + expect(parsePreviewBaseDomain("not-a-url")).toBeNull(); + }); + + it("returns null when the templated form has no base", () => { + // `{handle}.localhost` — strip leading subdomain leaves "localhost", + // which is technically valid, but `{handle}` alone (no dot) isn't. + expect(parsePreviewBaseDomain("https://{handle}")).toBeNull(); + }); +}); + +describe("extractHandleFromHost", () => { + const base = "preview.decocms.com"; + + it("extracts mesh-sb- handles from the matching subdomain", () => { + expect( + extractHandleFromHost("mesh-sb-abc123.preview.decocms.com", base), + ).toBe("mesh-sb-abc123"); + }); + + it("ignores port suffix in Host header", () => { + expect( + extractHandleFromHost("mesh-sb-abc.preview.decocms.com:8080", base), + ).toBe("mesh-sb-abc"); + }); + + it("is case-insensitive on host + base", () => { + expect(extractHandleFromHost("Mesh-Sb-ABC.Preview.DecocMs.com", base)).toBe( + "mesh-sb-abc", + ); + }); + + it("returns null when the handle prefix is missing", () => { + expect( + extractHandleFromHost("randomthing.preview.decocms.com", base), + ).toBeNull(); + }); + + it("returns null when the base domain doesn't match", () => { + expect( + extractHandleFromHost("mesh-sb-abc.preview.example.org", base), + ).toBeNull(); + }); + + it("rejects nested subdomains", () => { + // foo.mesh-sb-abc.preview.decocms.com → strip suffix yields + // "foo.mesh-sb-abc" which has a dot → null. + expect( + extractHandleFromHost("foo.mesh-sb-abc.preview.decocms.com", base), + ).toBeNull(); + }); + + it("returns null for missing host or base", () => { + expect(extractHandleFromHost(null, base)).toBeNull(); + expect(extractHandleFromHost(undefined, base)).toBeNull(); + expect( + extractHandleFromHost("mesh-sb-abc.preview.decocms.com", ""), + ).toBeNull(); + }); +}); + +describe("tryHandlePreviewHttp", () => { + const baseDomain = "preview.example.com"; + + it("returns null when the host doesn't match a preview URL", async () => { + const req = new Request("https://api.example.com/foo", { + headers: { host: "api.example.com" }, + }); + const res = await tryHandlePreviewHttp(req, { + baseDomain, + getRunner: async () => null, + }); + expect(res).toBeNull(); + }); + + it("returns 503 when the runner isn't configured for K8s", async () => { + const req = new Request("https://mesh-sb-abc.preview.example.com/", { + headers: { host: "mesh-sb-abc.preview.example.com" }, + }); + const res = await tryHandlePreviewHttp(req, { + baseDomain, + getRunner: async () => null, + }); + expect(res).not.toBeNull(); + expect(res!.status).toBe(503); + }); + + it("delegates to runner.proxyPreviewRequest with the parsed handle", async () => { + let received: { handle: string; req: Request } | null = null; + const fakeRunner = { + proxyPreviewRequest: async (handle: string, req: Request) => { + received = { handle, req }; + return new Response("ok", { status: 200 }); + }, + }; + const req = new Request( + "https://mesh-sb-deadbeef.preview.example.com/foo", + { + headers: { host: "mesh-sb-deadbeef.preview.example.com" }, + }, + ); + const res = await tryHandlePreviewHttp(req, { + baseDomain, + // biome-ignore lint/suspicious/noExplicitAny: structural duck-type + getRunner: async () => fakeRunner as any, + }); + expect(res).not.toBeNull(); + expect(res!.status).toBe(200); + expect(received).not.toBeNull(); + expect(received!.handle).toBe("mesh-sb-deadbeef"); + }); +}); + +describe("tryUpgradePreviewWs", () => { + const baseDomain = "preview.example.com"; + const previewHost = "mesh-sb-abc123.preview.example.com"; + + function wsRequest(path: string, host: string = previewHost): Request { + return new Request(`https://${host}${path}`, { + headers: { + host, + upgrade: "websocket", + connection: "upgrade", + "sec-websocket-key": "x3JJHMbDL1EzLkh9GBhXDw==", + "sec-websocket-version": "13", + }, + }); + } + + it("returns null when not a WS upgrade", async () => { + const req = new Request(`https://${previewHost}/`, { + headers: { host: previewHost }, + }); + const res = await tryUpgradePreviewWs( + req, + { upgrade: () => true }, + { baseDomain, getRunner: async () => null }, + ); + expect(res).toBeNull(); + }); + + it("returns null when host doesn't match a preview", async () => { + const req = wsRequest("/", "api.example.com"); + const res = await tryUpgradePreviewWs( + req, + { upgrade: () => true }, + { baseDomain, getRunner: async () => null }, + ); + expect(res).toBeNull(); + }); + + it("returns 503 when the runner isn't ready", async () => { + const req = wsRequest("/"); + const res = await tryUpgradePreviewWs( + req, + { upgrade: () => true }, + { baseDomain, getRunner: async () => null }, + ); + expect(res).not.toBeNull(); + expect((res as Response).status).toBe(503); + }); + + it("returns 404 when sandbox lookup misses", async () => { + const fakeRunner = { + resolvePreviewUpstreamUrl: async () => null, + }; + const req = wsRequest("/"); + const res = await tryUpgradePreviewWs( + req, + { upgrade: () => true }, + { + baseDomain, + // biome-ignore lint/suspicious/noExplicitAny: structural duck-type + getRunner: async () => fakeRunner as any, + }, + ); + expect(res).not.toBeNull(); + expect((res as Response).status).toBe(404); + }); + + it("rejects /_decopilot_vm/* paths even on WS", async () => { + const fakeRunner = { + resolvePreviewUpstreamUrl: async () => "http://x:9000", + }; + const req = wsRequest("/_decopilot_vm/bash"); + const res = await tryUpgradePreviewWs( + req, + { upgrade: () => true }, + { + baseDomain, + // biome-ignore lint/suspicious/noExplicitAny: structural duck-type + getRunner: async () => fakeRunner as any, + }, + ); + expect(res).not.toBeNull(); + expect((res as Response).status).toBe(404); + }); + + it("calls server.upgrade and returns undefined when upgrade succeeds", async () => { + const fakeRunner = { + resolvePreviewUpstreamUrl: async () => "http://upstream:9000", + }; + let upgradeArgs: { req: Request; data: unknown } | null = null; + const server = { + upgrade: (req: Request, opts?: { data?: unknown }) => { + upgradeArgs = { req, data: opts?.data }; + return true; + }, + }; + const req = wsRequest("/__vite-hmr"); + const res = await tryUpgradePreviewWs(req, server, { + baseDomain, + // biome-ignore lint/suspicious/noExplicitAny: structural duck-type + getRunner: async () => fakeRunner as any, + }); + expect(res).toBeUndefined(); + expect(upgradeArgs).not.toBeNull(); + const data = upgradeArgs!.data as { upstreamUrl: string; kind: string }; + expect(data.kind).toBe("preview"); + expect(data.upstreamUrl).toBe("ws://upstream:9000/__vite-hmr"); + }); +}); diff --git a/apps/mesh/src/sandbox/preview-proxy.ts b/apps/mesh/src/sandbox/preview-proxy.ts new file mode 100644 index 0000000000..7dbe5df549 --- /dev/null +++ b/apps/mesh/src/sandbox/preview-proxy.ts @@ -0,0 +1,317 @@ +/** + * Sandbox preview reverse-proxy. + * + * Inbound requests to `.preview.` are routed to the + * matching sandbox's daemon at port 9000. Mesh stays in the request path + * for the first ship; long-term plan is per-claim HTTPRoute objects (see + * the K8s sandbox plan), but this keeps DNS + RBAC simple while we ship. + * + * Why preview must terminate on port 9000 and never on the in-pod dev port + * (3000): the daemon's reverse proxy strips CSP/X-Frame headers and injects + * the HMR bootstrap that vite needs to function inside the studio iframe. + * Routing browsers straight at the dev port breaks SSE + iframe embedding. + * + * Auth model: preview URLs are open-by-handle, the same way Vercel preview + * URLs are. The handle is the secret. /_decopilot_vm/* is rejected here + * (defense-in-depth — the daemon's bearer-token check rejects it too) so + * the admin surface stays uncallable from preview hosts. + */ + +import { + HANDLE_PREFIX, + type KubernetesSandboxRunner, +} from "@decocms/sandbox/runner/k8s"; + +/** + * Parses the base preview hostname (e.g. `preview.decocms.com`) out of the + * `MESH_SANDBOX_PREVIEW_URL_PATTERN` value. The pattern has the form + * `https://{handle}.preview.example.com` (or `https://{handle}.`), + * matching what the K8s runner's `applyPreviewPattern` produces. Returns + * null when the pattern is empty/missing/malformed — preview proxying is + * disabled in that case. + */ +export function parsePreviewBaseDomain( + pattern: string | null | undefined, +): string | null { + if (!pattern || pattern.trim() === "") return null; + // Substituting a placeholder before parsing handles the `{handle}` form. + // For the non-templated form we still get a valid URL whose hostname is + // the base. + const probe = pattern.includes("{handle}") + ? pattern.replace("{handle}", "__handle__") + : pattern; + let url: URL; + try { + url = new URL(probe); + } catch { + return null; + } + // `__handle__.preview.example.com` → strip the leading subdomain to get the + // base. If there's no leading subdomain segment, the pattern was bad. + const host = url.hostname; + if (pattern.includes("{handle}")) { + const dot = host.indexOf("."); + if (dot <= 0 || dot === host.length - 1) return null; + return host.slice(dot + 1); + } + // Bare-pattern form (no `{handle}`): `https://preview.example.com` — the + // hostname *is* the base. The runner's applyPreviewPattern in this case + // emits `https://.preview.example.com`. + return host; +} + +/** + * Pulls the sandbox handle out of a request Host header. Returns null when + * the host doesn't match `.` or the handle doesn't carry + * the K8s runner's `mesh-sb-` prefix (anything else means the request isn't + * for a mesh sandbox preview and should fall through to the rest of the + * mesh API). + */ +export function extractHandleFromHost( + host: string | null | undefined, + baseDomain: string, +): string | null { + if (!host || !baseDomain) return null; + const colon = host.indexOf(":"); + const cleanHost = (colon >= 0 ? host.slice(0, colon) : host).toLowerCase(); + const cleanBase = baseDomain.toLowerCase().replace(/^\.+|\.+$/g, ""); + const suffix = `.${cleanBase}`; + if (!cleanHost.endsWith(suffix)) return null; + const handle = cleanHost.slice(0, cleanHost.length - suffix.length); + // Reject empty / nested subdomains: `foo.bar.preview.example.com` would be + // `foo.bar`, which is not a valid handle. + if (!handle || handle.includes(".")) return null; + if (!handle.startsWith(HANDLE_PREFIX)) return null; + return handle; +} + +export interface PreviewProxyDeps { + /** + * Lazy runner accessor. Returns null when the mesh isn't configured for + * the K8s runner — the caller treats null as "not a preview deployment" + * and falls through. + */ + getRunner: () => Promise; + baseDomain: string; +} + +/** + * Returns a Response if the request was a preview request (handled here), + * otherwise null (caller should fall through to its normal routing). + * + * 503 is returned when the runner isn't ready yet — preview traffic hit the + * mesh before any sandbox tool initialized the runner. The browser will + * retry; by then the runner should be up. + */ +export async function tryHandlePreviewHttp( + request: Request, + deps: PreviewProxyDeps, +): Promise { + const handle = extractHandleFromHost( + request.headers.get("host"), + deps.baseDomain, + ); + if (!handle) return null; + + const runner = await deps.getRunner(); + if (!runner) { + return errorResponse(503, "preview proxy not configured"); + } + return runner.proxyPreviewRequest(handle, request); +} + +// Cross-origin error envelope. Studio runs under its own origin and reads +// these via fetch (EventSource probeMissing, SSE error frames); without ACAO +// the browser hides the status and devtools surfaces an opaque CORS failure. +function errorResponse(status: number, message: string): Response { + return new Response(JSON.stringify({ error: message }), { + status, + headers: { + "content-type": "application/json", + "access-control-allow-origin": "*", + }, + }); +} + +/** + * WebSocket upgrade payload — Bun's `server.upgrade()` stashes this under + * `ws.data` for the websocket handler to use. Keeping the upstream URL + + * subprotocols here means the handler doesn't need to re-parse the host. + */ +export interface PreviewWsData { + kind: "preview"; + upstreamUrl: string; + upstreamProtocols: string[]; + /** Buffer messages received before the upstream WS finishes opening. */ + pending: Array; + upstream: WebSocket | null; + closed: boolean; +} + +export function isPreviewWsData(data: unknown): data is PreviewWsData { + return ( + typeof data === "object" && + data !== null && + (data as { kind?: unknown }).kind === "preview" + ); +} + +/** + * Bun-specific upgrade interceptor: consumed by the top-level Bun.serve + * fetch handler. Returns: + * - undefined when the request was upgraded (Bun.serve treats this as + * "the response will come from the WS handler later") + * - a Response when the request matched preview but couldn't be upgraded + * (404/502/503), letting the caller return it directly + * - null when the request isn't a preview WS request (caller falls through) + * + * Only handles `Upgrade: websocket` requests. Plain HTTP/SSE goes through + * `tryHandlePreviewHttp` instead. + */ +export async function tryUpgradePreviewWs( + request: Request, + server: BunServerLike, + deps: PreviewProxyDeps, +): Promise { + if ((request.headers.get("upgrade") ?? "").toLowerCase() !== "websocket") { + return null; + } + const handle = extractHandleFromHost( + request.headers.get("host"), + deps.baseDomain, + ); + if (!handle) return null; + + const runner = await deps.getRunner(); + if (!runner) { + return errorResponse(503, "preview proxy not configured"); + } + + const upstreamHttp = await runner.resolvePreviewUpstreamUrl(handle); + if (!upstreamHttp) { + return errorResponse(404, "sandbox not found"); + } + + const reqUrl = new URL(request.url); + if (reqUrl.pathname.startsWith("/_decopilot_vm")) { + return errorResponse(404, "not found"); + } + + const upstreamUrl = `${upstreamHttp.replace(/^http/, "ws")}${reqUrl.pathname}${reqUrl.search}`; + const protocolHeader = request.headers.get("sec-websocket-protocol"); + const upstreamProtocols = protocolHeader + ? protocolHeader + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + : []; + + const data: PreviewWsData = { + kind: "preview", + upstreamUrl, + upstreamProtocols, + pending: [], + upstream: null, + closed: false, + }; + + const upgraded = server.upgrade(request, { data }); + if (!upgraded) { + return errorResponse(426, "upgrade failed"); + } + return undefined; +} + +/** + * Bun WebSocket handler for the upgraded preview connection. Pumps frames + * between the browser side (`ws`) and the upstream daemon (`ws.data.upstream`) + * in both directions. Buffers inbound frames received before the upstream + * dial completes — Bun delivers messages on `ws` immediately after upgrade, + * and the upstream WebSocket handshake takes a non-zero number of ticks. + */ +export const previewWebSocketHandler = { + open(ws: PreviewServerWebSocket) { + const data = ws.data; + if (!isPreviewWsData(data)) return; + let upstream: WebSocket; + try { + upstream = + data.upstreamProtocols.length > 0 + ? new WebSocket(data.upstreamUrl, data.upstreamProtocols) + : new WebSocket(data.upstreamUrl); + } catch (err) { + console.warn( + `[preview-ws] failed to dial upstream ${data.upstreamUrl}: ${err instanceof Error ? err.message : String(err)}`, + ); + data.closed = true; + try { + ws.close(1011, "upstream connect failed"); + } catch {} + return; + } + upstream.binaryType = "arraybuffer"; + data.upstream = upstream; + + upstream.addEventListener("open", () => { + while (data.pending.length > 0) { + const msg = data.pending.shift(); + if (msg !== undefined) upstream.send(msg); + } + }); + upstream.addEventListener("message", (ev: MessageEvent) => { + if (data.closed) return; + ws.send(ev.data as string | Uint8Array | ArrayBuffer); + }); + upstream.addEventListener("close", (ev: CloseEvent) => { + if (data.closed) return; + data.closed = true; + try { + ws.close(ev.code || 1000, ev.reason || ""); + } catch {} + }); + upstream.addEventListener("error", () => { + if (data.closed) return; + data.closed = true; + try { + ws.close(1011, "upstream error"); + } catch {} + }); + }, + message( + ws: PreviewServerWebSocket, + message: string | Uint8Array | ArrayBuffer, + ) { + const data = ws.data; + if (!isPreviewWsData(data)) return; + const upstream = data.upstream; + if (upstream && upstream.readyState === WebSocket.OPEN) { + upstream.send(message); + } else { + data.pending.push(message); + } + }, + close(ws: PreviewServerWebSocket) { + const data = ws.data; + if (!isPreviewWsData(data)) return; + data.closed = true; + try { + data.upstream?.close(); + } catch {} + }, +}; + +// Minimal structural types to avoid taking a hard dependency on `bun-types` +// in this module. The real Bun.ServerWebSocket / Bun.Server are wider but +// we only touch these members. +export interface PreviewServerWebSocket { + data: PreviewWsData | unknown; + send(data: string | Uint8Array | ArrayBuffer): number; + close(code?: number, reason?: string): void; +} + +export interface BunServerLike { + upgrade( + request: Request, + options?: { data?: unknown; headers?: HeadersInit }, + ): boolean; +} diff --git a/apps/mesh/src/tools/vm/start.ts b/apps/mesh/src/tools/vm/start.ts index a244182af6..6e7eaa0710 100644 --- a/apps/mesh/src/tools/vm/start.ts +++ b/apps/mesh/src/tools/vm/start.ts @@ -230,6 +230,7 @@ async function provisionSandbox( displayName: `${githubRepo.owner}/${githubRepo.name}`, }, workload, + tenant: { orgId, userId }, }, ); diff --git a/deploy/helm/README.md b/deploy/helm/README.md index 6f6e1277eb..12fe48970a 100644 --- a/deploy/helm/README.md +++ b/deploy/helm/README.md @@ -1383,10 +1383,105 @@ emits zero sandbox-related resources. - Cluster capacity for sandbox pods. Defaults request `500m` CPU / `1Gi` memory per sandbox and cap at `2` CPU / `4Gi` / `10Gi` ephemeral. Tune via `sandbox.kubernetes.resources.*`. -- If you run previews through an ingress gateway (Istio, NGINX, Gateway - API), set `sandbox.kubernetes.networkPolicy.previewGatewayNamespace` to - the namespace holding the gateway controller's pods so port 3000 ingress - is allowed. +- For preview URLs (`*.preview.`), see + [Sandbox preview ingress](#sandbox-preview-ingress) below — this is the + standard path and uses the Gateway API + cert-manager. +- The legacy + `sandbox.kubernetes.networkPolicy.previewGatewayNamespace` knob is only + needed for setups that route preview traffic *around* mesh, terminating + directly on the sandbox's port 3000. The standard path lands on port + 9000 via mesh, where the daemon's CSP/HMR rewrites apply. + +### Sandbox preview ingress + +When `sandbox.kubernetes.previewGateway.enabled=true`, the chart renders +an Istio Gateway + HTTPRoute + cert-manager Certificate that send +`*.preview.` traffic to the mesh Service. Mesh recognises the +Host header and reverse-proxies to the matching sandbox's daemon at +port 9000 — including WebSocket upgrades, so vite HMR works. + +Required values: + +```yaml +sandbox: + kubernetes: + enabled: true + previewUrlPattern: "https://{handle}.preview.example.com" + previewGateway: + enabled: true + domain: "preview.example.com" + clusterIssuer: "cloudflare-dns01" # name of an existing ClusterIssuer + # Optional overrides: + # gatewayClassName: "istio" + # namespace: "istio-system" +``` + +Two things are *not* templated and have to be done by hand once per +cluster — the chart will not work end-to-end without these: + +#### 1. DNS — wildcard A/CNAME + +In your DNS provider (Cloudflare, Route53, etc.), add a wildcard record +pointing at the cluster's external load balancer: + +``` +*.preview.example.com → +``` + +For Cloudflare, set the record to **DNS only** (grey-cloud, not orange) +so cert-manager's DNS-01 challenge can update TXT records under the +zone. Cloudflare proxy mode (orange-cloud) blocks DNS-01. + +To find the LB hostname for an Istio Gateway: + +```bash +kubectl get svc -n istio-system # look for the LoadBalancer service +``` + +#### 2. cert-manager DNS-01 ClusterIssuer + +DNS-01 is the only solver that works for wildcard certs. The chart does +not template the ClusterIssuer because the API token is per-cluster +infrastructure. Example for Cloudflare: + +```yaml +# Apply to the cluster ONCE — outside the chart. +apiVersion: v1 +kind: Secret +metadata: + name: cloudflare-api-token + namespace: cert-manager +type: Opaque +stringData: + api-token: "" +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: cloudflare-dns01 +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: admin@example.com + privateKeySecretRef: + name: cloudflare-dns01-account-key + solvers: + - dns01: + cloudflare: + apiTokenSecretRef: + name: cloudflare-api-token + key: api-token +``` + +Verify the cert provisions after `helm upgrade`: + +```bash +kubectl get certificate -n istio-system +kubectl describe certificate -sandbox-preview -n istio-system +``` + +cert-manager logs in `cert-manager` namespace are the place to look if +the cert hangs in `Pending` for more than a few minutes. ### Local kind diff --git a/deploy/helm/examples/postgres-kind.yaml b/deploy/helm/examples/postgres-kind.yaml new file mode 100644 index 0000000000..42f956f598 --- /dev/null +++ b/deploy/helm/examples/postgres-kind.yaml @@ -0,0 +1,81 @@ +# Minimal Postgres for kind dev installs. +# +# The chart-deco-studio chart's `database.engine: sqlite` mode is a relic of +# the published `decocms` npm package — the current source code uses Kysely + +# node-postgres exclusively. So a postgres instance has to exist somewhere +# the studio pod can reach. +# +# This manifest stands one up in the same namespace as the helm release. Pair +# with values-kind.yaml's `database.engine: postgresql` + `database.url` +# overrides. NOT for production — no auth hardening, no backups, single +# replica. Replace with a managed RDS / CloudSQL / external Postgres URL in +# any real deploy. +# +# Apply with: +# kubectl apply -n deco-studio -f deploy/helm/examples/postgres-kind.yaml +apiVersion: v1 +kind: Service +metadata: + name: postgres +spec: + selector: + app: postgres + ports: + - port: 5432 + targetPort: 5432 + clusterIP: None # headless — DNS resolves directly to the pod +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-data +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi + # storageClassName omitted → cluster default (`standard` on kind). +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres +spec: + replicas: 1 + strategy: + type: Recreate # SQLite-style: single writer, RWO PVC + selector: + matchLabels: + app: postgres + template: + metadata: + labels: + app: postgres + spec: + containers: + - name: postgres + image: postgres:17-alpine + env: + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_DB + value: postgres + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + readinessProbe: + exec: + command: ["pg_isready", "-U", "postgres"] + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: data + persistentVolumeClaim: + claimName: postgres-data diff --git a/deploy/helm/examples/values-kind.yaml b/deploy/helm/examples/values-kind.yaml new file mode 100644 index 0000000000..f659f794d5 --- /dev/null +++ b/deploy/helm/examples/values-kind.yaml @@ -0,0 +1,152 @@ +# Local kind cluster overrides for chart-deco-studio. +# +# Use with the runbook in deploy/helm/examples/README-kind.md (or paste from +# the chat). Designed to work on macOS (arm64 / Apple Silicon) AND linux/amd64 +# without changes — no nodeSelector pins, default StorageClass. +# +# Pairs with: +# - sandbox.kubernetes.enabled=true → installs the agent-sandbox subchart +# so the helm release fully owns the operator + CRDs (no separate +# deploy/k8s-sandbox/local/up.sh step). +# - mesh-sandbox:local image built locally and `kind load`ed into the cluster +# before `helm install`. + +# ── single replica, SQLite, no autoscaling ───────────────────────────── +replicaCount: 1 + +# Studio image: built from current branch source via tests/resilience/ +# Dockerfile.studio and `kind load`ed. Two reasons not to use the published +# image: +# 1. `ghcr.io/decocms/studio/studio` is private — anonymous pull returns 403. +# 2. The published `decocms` npm package may lag the current branch (e.g. +# MESH_SANDBOX_RUNNER=kubernetes wasn't recognized in 2.281.2). Building +# from source guarantees we're testing the code under development. +image: + repository: deco-studio + tag: local + pullPolicy: Never + # Source build doesn't install the `decocms` npm package, so the default + # chart command (`bun run deco …`) won't resolve. Use cli.ts directly: + # it goes through buildSettings → ensureServices, which spawns embedded + # postgres + runs Better Auth + Kysely migrations end-to-end. The + # `dist/server/migrate.js` path the chart's sqlite branch wires up + # bypasses ensureServices and tries to pg-connect to DATABASE_URL + # literally, which fails on the chart's `/app/data/mesh.db` placeholder. + command: + - bun + - run + - src/cli.ts + - --no-tui + - --no-local-mode + +# ── extra env: TLS workaround for Bun + K8s port-forward ───────────── +# @kubernetes/client-node's PortForward uses the `ws` package for the +# WebSocket upgrade. Bun's `ws` build doesn't honor the Node-style `ca` +# option, so the WS handshake fails TLS validation against the kind API +# server's self-signed cert. Plain HTTPS calls (claim CRUD) work fine — +# only WS-backed port-forward breaks. Symptom: studio sees +# `[KubernetesSandboxRunner] port-forward to :9000 failed: [object +# ErrorEvent]` and the daemon /health probe times out. +# +# Workaround for kind dev only: disable TLS verification globally for the +# pod. NEVER set this in prod — it disables all cert validation on the +# studio process. Track upstream Bun fix or switch the studio image to +# Node.js if you need to run @kubernetes/client-node port-forward +# securely in production. +env: + - name: NODE_TLS_REJECT_UNAUTHORIZED + value: "0" + +# ── database ────────────────────────────────────────────────────────── +# Use the in-cluster postgres deployed via examples/postgres-kind.yaml. +# The chart's `sqlite` mode is incompatible with the current branch (the +# bundled migrate.js opens pg-pool against DATABASE_URL literally and +# crashes when handed `/app/data/mesh.db`). +database: + engine: postgresql + url: "postgresql://postgres:postgres@postgres.deco-studio.svc.cluster.local:5432/postgres" + +# Single-node kind cluster has no zones. Drop the spread constraint to +# keep `kubectl describe` output clean. +topologySpreadConstraints: [] + +# Tighten requests so the cluster fits comfortably in Docker Desktop's +# default ~4Gi. Bump back up when stress-testing. +resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + +# ── storage ─────────────────────────────────────────────────────────── +# kind ships rancher.io/local-path as the default StorageClass. +# Empty string = use whatever the cluster marks default (= local-path here). +persistence: + enabled: true + accessMode: ReadWriteOnce + # Empty = chart omits storageClassName, K8s falls back to the default + # StorageClass (`standard` / rancher.io/local-path on kind). + storageClass: "" + size: 5Gi + distributed: false + +# SQLite + ReadWriteOnce → chart auto-detects Recreate strategy. Leave +# strategy unset. + +# ── secrets (override on the command line, NOT here) ────────────────── +# Pass via --set at install time: +# --set secret.BETTER_AUTH_SECRET="$(openssl rand -base64 32)" +# --set secret.ENCRYPTION_KEY="$(openssl rand -base64 32)" + +# ── NATS subchart ───────────────────────────────────────────────────── +# Keep enabled (mesh's event bus needs it). PVC storageClassName is +# already empty in the parent default; that resolves to local-path on kind. + +# ── k8s sandbox runner ──────────────────────────────────────────────── +sandbox: + kubernetes: + enabled: true + image: + # Built locally via packages/sandbox + `kind load docker-image`. + # See runbook for the exact build command. + repository: mesh-sandbox + tag: local + pullPolicy: Never + # Modest sandbox limits to fit alongside studio + NATS on a laptop + # kind cluster. Mirrors deploy/k8s-sandbox/local/sandbox-template.yaml. + resources: + requests: + cpu: "100m" + memory: "512Mi" + limits: + cpu: "1" + memory: "3Gi" + networkPolicy: + # kindnet enforces NetworkPolicy as of kind v0.27 (kindnetd v1.x). + # Leave the chart's policy on so studio → sandbox traffic on port + # 9000 is explicitly allow-listed; otherwise the operator-managed + # default (only `app: sandbox-router` ingress, locked off via + # `networkPolicyManagement: Unmanaged` in the chart's SandboxTemplate) + # would have re-blocked everything. Older kindnet builds (pre-0.27) + # ignore the policy and the rule is inert — safe either way. + enabled: true + # No Istio in kind → no preview gateway. Mesh's HTTP edge handles preview + # routing in-process via apps/mesh/src/sandbox/preview-proxy.ts: it reads + # the Host header, extracts the sandbox handle, and reverse-proxies to + # the in-cluster daemon Service. + previewGateway: + enabled: false + # `*.localhost` is special — browsers resolve every subdomain of + # localhost to 127.0.0.1 without /etc/hosts entries. Combined with the + # user's `kubectl port-forward svc/deco-studio 8080:80`, this means + # browser → `mesh-sb-XXX.preview.localhost:8080` → kube port-forward → + # mesh edge → preview-proxy → sandbox daemon Service. No ingress, no DNS, + # no certs. Match the port to whatever the user port-forwards to. + previewUrlPattern: "http://{handle}.preview.localhost:8080" + # Plain host-users mode. userns remap requires K8s 1.30+ on a kernel + # that supports it; kind nodes can vary. Keep simple for local dev. + hostUsers: true + readOnlyRootFilesystem: false + warmPool: + enabled: false diff --git a/deploy/helm/templates/pvc.yaml b/deploy/helm/templates/pvc.yaml index dfcd2148f5..c98c4b84fb 100644 --- a/deploy/helm/templates/pvc.yaml +++ b/deploy/helm/templates/pvc.yaml @@ -12,10 +12,8 @@ spec: resources: requests: storage: {{ .Values.persistence.size }} - {{- if .Values.persistence.storageClass }} - storageClassName: {{ .Values.persistence.storageClass }} - {{- else }} - storageClassName: "" # Use default storage class + {{- with .Values.persistence.storageClass }} + storageClassName: {{ . }} {{- end }} {{- end }} {{- end }} diff --git a/deploy/helm/templates/sandbox-network-policy.yaml b/deploy/helm/templates/sandbox-network-policy.yaml index 2fda883ab0..5ab36fb680 100644 --- a/deploy/helm/templates/sandbox-network-policy.yaml +++ b/deploy/helm/templates/sandbox-network-policy.yaml @@ -31,8 +31,11 @@ spec: - Egress ingress: # Daemon port (9000) — mesh server pods call this to exec tools, stream - # logs, etc. Matches the chart's own selectorLabels so self-hosters who - # use the default deployment get the rule automatically. + # logs, etc. Also the *preview* request path: when previewGateway is + # enabled, mesh reverse-proxies `*.preview.` traffic to the + # daemon here so the daemon's CSP/HMR rewrites apply (port 3000 would + # bypass them). Matches the chart's own selectorLabels so self-hosters + # who use the default deployment get the rule automatically. - from: - namespaceSelector: matchLabels: @@ -44,8 +47,10 @@ spec: - protocol: TCP port: 9000 {{- with .Values.sandbox.kubernetes.networkPolicy.previewGatewayNamespace }} - # Dev server port (3000) — preview URLs terminate on an ingress gateway - # (Istio / Gateway API / NGINX) in the configured namespace. + # Legacy/standby — direct ingress on dev port 3000 from a configured + # gateway namespace. Only needed for setups that route preview traffic + # *around* mesh (no daemon CSP/HMR rewrites). The standard Istio + # Gateway API path lands on port 9000 via mesh and doesn't need this. - from: - namespaceSelector: matchLabels: diff --git a/deploy/helm/templates/sandbox-preview-cert.yaml b/deploy/helm/templates/sandbox-preview-cert.yaml new file mode 100644 index 0000000000..87d5903587 --- /dev/null +++ b/deploy/helm/templates/sandbox-preview-cert.yaml @@ -0,0 +1,34 @@ +{{- if and .Values.sandbox.kubernetes.enabled .Values.sandbox.kubernetes.previewGateway.enabled }} +{{- $domain := required "sandbox.kubernetes.previewGateway.domain is required when previewGateway.enabled=true" .Values.sandbox.kubernetes.previewGateway.domain }} +{{- $issuer := required "sandbox.kubernetes.previewGateway.clusterIssuer is required when previewGateway.enabled=true" .Values.sandbox.kubernetes.previewGateway.clusterIssuer }} +{{- $gwNamespace := .Values.sandbox.kubernetes.previewGateway.namespace }} +{{- $tlsSecretName := default (printf "%s-sandbox-preview-tls" (include "chart-deco-studio.fullname" .)) .Values.sandbox.kubernetes.previewGateway.tlsSecretName }} +{{- $meshServiceName := include "chart-deco-studio.fullname" . }} +# Wildcard cert for the sandbox preview Gateway. cert-manager places the +# Secret in the gateway namespace so the Gateway listener can mount it +# without a cross-namespace reference. +# +# DNS-01 is the only solver that can validate a wildcard SAN, so the +# referenced ClusterIssuer must be DNS-01 (e.g. Cloudflare, Route53). The +# chart does not template the ClusterIssuer itself — the API tokens +# required to provision DNS records are per-cluster infra, not chart +# config. See deploy/helm/README.md for a Cloudflare DNS-01 ClusterIssuer +# template. +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ $meshServiceName }}-sandbox-preview + namespace: {{ $gwNamespace }} + labels: + app.kubernetes.io/name: mesh-sandbox-preview + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "chart-deco-studio.chart" . }} +spec: + secretName: {{ $tlsSecretName }} + issuerRef: + kind: ClusterIssuer + name: {{ $issuer | quote }} + commonName: {{ printf "*.%s" $domain | quote }} + dnsNames: + - {{ printf "*.%s" $domain | quote }} +{{- end }} diff --git a/deploy/helm/templates/sandbox-preview-gateway.yaml b/deploy/helm/templates/sandbox-preview-gateway.yaml new file mode 100644 index 0000000000..157783716a --- /dev/null +++ b/deploy/helm/templates/sandbox-preview-gateway.yaml @@ -0,0 +1,78 @@ +{{- if and .Values.sandbox.kubernetes.enabled .Values.sandbox.kubernetes.previewGateway.enabled }} +{{- $domain := required "sandbox.kubernetes.previewGateway.domain is required when previewGateway.enabled=true" .Values.sandbox.kubernetes.previewGateway.domain }} +{{- $issuer := required "sandbox.kubernetes.previewGateway.clusterIssuer is required when previewGateway.enabled=true" .Values.sandbox.kubernetes.previewGateway.clusterIssuer }} +{{- $gwNamespace := .Values.sandbox.kubernetes.previewGateway.namespace }} +{{- $tlsSecretName := default (printf "%s-sandbox-preview-tls" (include "chart-deco-studio.fullname" .)) .Values.sandbox.kubernetes.previewGateway.tlsSecretName }} +{{- $hostname := printf "*.%s" $domain }} +{{- $meshServiceName := include "chart-deco-studio.fullname" . }} +# Wildcard preview-URL ingress: Approach B from the K8s sandbox plan. A +# single Gateway + HTTPRoute terminate `*.preview.` and forward to +# the mesh Service; mesh inspects the Host header and reverse-proxies to +# the matching sandbox's daemon at port 9000 (daemon owns the public +# surface; routing browsers straight at dev port 3000 would bypass the +# daemon's CSP/HMR rewrites and break iframe embedding + SSE). +# +# Mesh stays in the request path for the first ship; the longer-term plan +# is per-claim HTTPRoutes that bypass mesh entirely. Switching to that +# requires per-Service routing + RBAC for mesh to mint HTTPRoutes, which +# is deferred. +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: {{ $meshServiceName }}-sandbox-preview + namespace: {{ $gwNamespace }} + labels: + app.kubernetes.io/name: mesh-sandbox-preview + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "chart-deco-studio.chart" . }} + annotations: + # cert-manager picks up the cert from the listener's TLS secret ref; + # this annotation tells it which ClusterIssuer to use when minting + # the wildcard. Required because Gateway listeners don't have a + # built-in `issuerRef` field. + cert-manager.io/cluster-issuer: {{ $issuer | quote }} +spec: + gatewayClassName: {{ .Values.sandbox.kubernetes.previewGateway.gatewayClassName | quote }} + listeners: + - name: https + protocol: HTTPS + port: 443 + hostname: {{ $hostname | quote }} + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: {{ $tlsSecretName }} + namespace: {{ $gwNamespace }} + allowedRoutes: + namespaces: + # HTTPRoute lives in the release namespace (next to mesh), so + # we have to explicitly allow cross-namespace attachment from + # there. Without `from: All` plus a tight selector this would + # silently drop the route. + from: Selector + selector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $meshServiceName }}-sandbox-preview + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: mesh-sandbox-preview + app.kubernetes.io/managed-by: {{ .Release.Service }} + helm.sh/chart: {{ include "chart-deco-studio.chart" . }} +spec: + parentRefs: + - kind: Gateway + name: {{ $meshServiceName }}-sandbox-preview + namespace: {{ $gwNamespace }} + hostnames: + - {{ $hostname | quote }} + rules: + - backendRefs: + - name: {{ $meshServiceName }} + port: {{ .Values.service.port }} +{{- end }} diff --git a/deploy/helm/templates/sandbox-rbac.yaml b/deploy/helm/templates/sandbox-rbac.yaml index ba8227d1bb..f6cfdd44ef 100644 --- a/deploy/helm/templates/sandbox-rbac.yaml +++ b/deploy/helm/templates/sandbox-rbac.yaml @@ -30,7 +30,13 @@ rules: verbs: ["get"] - apiGroups: [""] resources: ["pods/portforward"] - verbs: ["create"] + # `get` is required for the WebSocket-based port-forward path used by + # @kubernetes/client-node v1.x (and Bun's native WS, and any newer + # client-go-equivalent). The legacy SPDY path only needed `create`, but + # all modern clients use the WS upgrade — which the API server enforces + # as a `GET` against the subresource. Without `get`, the upgrade returns + # 403 and the runner sees `[object ErrorEvent]`. + verbs: ["get", "create"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/deploy/helm/templates/sandbox-template.yaml b/deploy/helm/templates/sandbox-template.yaml index 4adaf4694b..2ae6c92d9f 100644 --- a/deploy/helm/templates/sandbox-template.yaml +++ b/deploy/helm/templates/sandbox-template.yaml @@ -20,10 +20,25 @@ spec: # template carries no shared secret; leakage of the template compromises # nothing on its own. envVarsInjectionPolicy: Allowed + # The CRD defaults to Managed, which makes the operator install its own + # NetworkPolicy that only allows ingress from pods labeled + # `app: sandbox-router`. That's intended for Istio-style sidecar routing + # and silently blocks the mesh → daemon path the preview-proxy depends on. + # We surface the netpol via templates/sandbox-network-policy.yaml instead + # (gated by `sandbox.kubernetes.networkPolicy.enabled`), so flag the + # operator's policy off. + networkPolicyManagement: Unmanaged podTemplate: metadata: labels: app.kubernetes.io/name: mesh-sandbox + # Do NOT set `mesh.decocms.com/role` here. The operator (v0.4.2+) + # rejects claims whose additionalPodMetadata defines a label key + # already present in the template — even when the values differ — + # with "metadata override conflict". The runner sets role=claimed + # via additionalPodMetadata, so the template must leave that key + # undefined. Warm-pool pods end up without the role label; + # dashboards filter by absence-of-handle instead. spec: automountServiceAccountToken: false {{- with .Values.sandbox.kubernetes.nodeSelector }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index d59646a7b0..e55594a5c7 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -129,8 +129,12 @@ terminationGracePeriodSeconds: 60 # Optional lifecycle hooks (preStop, postStart) # lifecycle: {} -nodeSelector: - kubernetes.io/arch: amd64 +nodeSelector: {} +# Pin arch in your prod values override if your node groups are heterogeneous. +# Default is unset so the chart works on arm64 (Apple Silicon kind, ARM EKS, +# bare-metal RPi clusters). Example for AWS amd64 node groups: +# nodeSelector: +# kubernetes.io/arch: amd64 tolerations: [] # Example: @@ -347,10 +351,11 @@ sandbox: ephemeral-storage: 10Gi networkPolicy: enabled: true - # If set, ingress from this namespace on port 3000 (dev server) is - # allowed — this is where the Istio / Gateway API controller pods live - # that terminate preview URLs. Leave empty to skip the rule (sandbox - # previews are then unreachable from outside the cluster). + # Legacy/standby — kept for setups whose external gateway terminates + # *directly* on the sandbox's port 3000 (dev server). The default + # preview path goes through mesh and lands on port 9000 (daemon) + # instead, so most installs leave this empty. See + # `previewGateway` below for the standard Istio Gateway API path. previewGatewayNamespace: "" # Public URL pattern that ingress terminates on for sandbox previews. # The runner substitutes {handle} (or whatever applyPreviewPattern @@ -358,8 +363,47 @@ sandbox: # gateway + cert is in place; leave empty in local dev so the runner # falls back to the 127.0.0.1 port-forward URL. Surfaced into the # mesh container as MESH_SANDBOX_PREVIEW_URL_PATTERN. + # + # Should match `previewGateway.domain` below — e.g. when + # previewGateway.domain="preview.decocms.com", set this to + # "https://{handle}.preview.decocms.com". previewUrlPattern: "" # previewUrlPattern: "https://{handle}.preview.example.com" + # Wildcard preview-URL ingress (Approach B in the K8s sandbox plan). + # Renders an Istio Gateway + HTTPRoute that send all + # *.preview. traffic to the mesh Service; mesh recognises the + # Host header and reverse-proxies to the matching sandbox's daemon at + # port 9000 (daemon, not dev port 3000 — the daemon's reverse proxy + # injects the HMR bootstrap + strips CSP that the iframe needs). + # + # Manual prerequisites (not templated): + # 1. DNS: Cloudflare (or other) wildcard `*.preview.` → + # cluster external LB hostname. + # 2. cert-manager ClusterIssuer for the wildcard cert. DNS-01 is + # required (HTTP-01 doesn't work for wildcards). Set + # `clusterIssuer` to that issuer's name. + previewGateway: + enabled: false + # gatewayClassName for the Gateway. EKS clusters running Istio + # ambient/sidecar default to "istio". Confirm with + # `kubectl get gatewayclasses` before flipping enabled=true. + gatewayClassName: "istio" + # Namespace where the Gateway + HTTPRoute land. Mesh's existing + # gateway typically lives in `istio-system`; some setups use a + # dedicated `gateway` ns. The cert Secret is created in the same ns. + namespace: "istio-system" + # Wildcard domain for previews — e.g. "preview.decocms.com" yields + # `*.preview.decocms.com`. Required when enabled=true. + domain: "" + # cert-manager ClusterIssuer that issues the wildcard cert. Required + # when enabled=true. The chart does NOT template the ClusterIssuer + # itself — that is per-cluster infrastructure (a Cloudflare DNS-01 + # issuer, for example, needs your API token in a Secret). + clusterIssuer: "" + # PEM-format secret name created by cert-manager. Defaults to + # `-sandbox-preview-tls`. Override only if the cert lives + # under a name dictated by external tooling. + tlsSecretName: "" # Pin sandbox pods to a dedicated NodePool so a container escape lands # on a node that has no mesh / postgres / NATS / OTel pods on it. Pair # with a Karpenter NodePool that taints + labels matching nodes; see diff --git a/deploy/k8s-sandbox/local/README.md b/deploy/k8s-sandbox/local/README.md index c1ebc2d391..a4fe76a8f9 100644 --- a/deploy/k8s-sandbox/local/README.md +++ b/deploy/k8s-sandbox/local/README.md @@ -4,20 +4,26 @@ Scripted local bring-up for `KubernetesSandboxRunner`. One-command cluster + agent-sandbox operator + mesh `SandboxTemplate`, loaded with the same sandbox image the Docker runner uses. -This is **dev ergonomics only** — no Helm, no Terraform, no `kubectl apply` -outside the scripts here. Prod/staging installs the operator via the deco -infrastructure repo, not these scripts. +This is **dev ergonomics only** — no Terraform. Prod/staging installs the +operator via the deco infrastructure repo, not these scripts. Helm is used +for upstream third-party stacks (Prometheus, Grafana, OpenTelemetry +Collector) since that's their canonical install path; mesh-owned manifests +(SandboxTemplate, agent-sandbox operator) stay raw `kubectl apply`. ## Prereqs - [`docker`](https://docs.docker.com/engine/install/) — running - [`kind`](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) - [`kubectl`](https://kubernetes.io/docs/tasks/tools/) +- [`helm`](https://helm.sh/docs/intro/install/) — required only for the + monitoring stack; skip with `MONITORING=0 ./up.sh` if not installed. Pins: - agent-sandbox operator: `v0.4.2` (matches prod; hardcoded in `up.sh`) +- kube-prometheus-stack: `65.5.1` +- opentelemetry-collector: `0.108.0` - cluster name: `mesh-sandbox-dev` -- namespace: `agent-sandbox-system` +- namespace: `agent-sandbox-system` (sandboxes), `monitoring` (Prom/Grafana/OTel) - image tag: `mesh-sandbox:local` ## Usage @@ -42,10 +48,72 @@ Pins: 5. Builds the daemon bundle (`bun run --cwd packages/sandbox build`), then `packages/sandbox/image/Dockerfile` as `mesh-sandbox:local` 6. Loads the image into kind (required because the template pins `imagePullPolicy: Never`) 7. Applies `sandbox-template.yaml` +8. Installs `kube-prometheus-stack` (Prom + Grafana + the operator that + discovers `ServiceMonitor`s) and the OTel Collector daemonset that + scrapes per-node kubelet, enriches with tenant labels, and exposes + `/metrics` for Prometheus. Skip with `MONITORING=0 ./up.sh`. All `kubectl` calls pass `--context kind-mesh-sandbox-dev` so an ambient `KUBECONFIG` can't accidentally hit a real cluster. +## Local Grafana + +After `up.sh`: + +```bash +kubectl --context kind-mesh-sandbox-dev port-forward \ + -n monitoring svc/kube-prometheus-stack-grafana 3001:80 +# → http://localhost:3001 (admin / admin) +# → Dashboards → "Mesh Sandbox Overview" +``` + +Dashboard panels (per-org, per-sandbox-handle): + +- Active sandboxes by org +- Egress rate by org +- CPU / memory by org +- Top 10 sandboxes by 1-hour egress +- Warm-pool overhead pod count (no owning org) + +The pipeline: + +``` +kubelet (cAdvisor) ──► OTel collector daemonset + │ - kubeletstats receiver + │ - k8sattributes processor (reads pod labels: + │ mesh.decocms.com/{org-id,user-id,sandbox-handle,role} + │ → series labels: org_id, user_id, sandbox_handle, sandbox_role) + │ - prometheus exporter on :8889 + ▼ + ServiceMonitor → kube-prometheus-stack Prometheus → Grafana +``` + +Pod labels come from `SandboxClaim.spec.additionalPodMetadata.labels`, +populated in `KubernetesSandboxRunner.provision()` from the `tenant` field +on `EnsureOptions`. Verify they're landing: + +```bash +kubectl --context kind-mesh-sandbox-dev \ + get pod -n agent-sandbox-system --show-labels | grep mesh.decocms.com +``` + +To iterate on dashboards/values without rebuilding the cluster: + +```bash +MONITORING_ONLY=1 ./deploy/k8s-sandbox/local/down.sh +./deploy/k8s-sandbox/local/up.sh +``` + +### Production swap + +The pipeline shape carries to prod with two changes: +1. Replace bundled Prometheus with remote-write to Mimir/VictoriaMetrics + (`prometheus.prometheusSpec.remoteWrite` in the values). +2. Restrict the k8sattributes processor's `filter.namespaces` to the + sandbox namespace once you're not also debugging system pods. + +Dashboards, label names, and metric names stay identical. + ## Smoke test Stage 1 exit criterion from PLAN-K8S-MVP.md. Exercises diff --git a/deploy/k8s-sandbox/local/down.sh b/deploy/k8s-sandbox/local/down.sh index 48eccd3e55..6b18e41b95 100755 --- a/deploy/k8s-sandbox/local/down.sh +++ b/deploy/k8s-sandbox/local/down.sh @@ -1,11 +1,28 @@ #!/usr/bin/env bash # Tear down the local kind cluster. Nothing outside kind is touched. +# +# `MONITORING_ONLY=1 ./down.sh` removes just the helm-installed monitoring +# stack — useful for iterating on values files without rebuilding the +# cluster + operator. Re-run up.sh to reinstall. set -euo pipefail CLUSTER_NAME="mesh-sandbox-dev" +KCTX="kind-${CLUSTER_NAME}" log() { printf "\033[1;34m[down]\033[0m %s\n" "$*"; } +if [[ "${MONITORING_ONLY:-0}" == "1" ]]; then + if ! kind get clusters 2>/dev/null | grep -qx "${CLUSTER_NAME}"; then + log "cluster ${CLUSTER_NAME} not found, nothing to remove" + exit 0 + fi + log "uninstalling monitoring stack only" + helm uninstall otel-collector-sandbox --namespace monitoring --kube-context "${KCTX}" >/dev/null 2>&1 || true + helm uninstall kube-prometheus-stack --namespace monitoring --kube-context "${KCTX}" >/dev/null 2>&1 || true + kubectl --context "${KCTX}" delete namespace monitoring --ignore-not-found + exit 0 +fi + if kind get clusters 2>/dev/null | grep -qx "${CLUSTER_NAME}"; then log "deleting kind cluster ${CLUSTER_NAME}" kind delete cluster --name "${CLUSTER_NAME}" diff --git a/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json b/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json new file mode 100644 index 0000000000..1a61a2b874 --- /dev/null +++ b/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json @@ -0,0 +1,115 @@ +{ + "title": "Mesh Sandbox Overview", + "uid": "mesh-sandbox-overview", + "schemaVersion": 39, + "version": 1, + "refresh": "30s", + "time": { "from": "now-1h", "to": "now" }, + "tags": ["mesh", "sandbox"], + "templating": { + "list": [ + { + "name": "namespace", + "type": "constant", + "current": { + "value": "agent-sandbox-system", + "text": "agent-sandbox-system" + }, + "query": "agent-sandbox-system", + "hide": 2 + } + ] + }, + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Active sandboxes (by org)", + "gridPos": { "h": 6, "w": 6, "x": 0, "y": 0 }, + "fieldConfig": { "defaults": { "unit": "none" } }, + "options": { + "reduceOptions": { "calcs": ["lastNotNull"] }, + "graphMode": "area", + "justifyMode": "auto" + }, + "targets": [ + { + "expr": "count by (org_id) (k8s_pod_cpu_usage{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "legendFormat": "{{org_id}}", + "refId": "A" + } + ] + }, + { + "id": 2, + "type": "stat", + "title": "Warm-pool overhead pods (no org)", + "gridPos": { "h": 6, "w": 6, "x": 6, "y": 0 }, + "fieldConfig": { "defaults": { "unit": "none" } }, + "targets": [ + { + "expr": "count(k8s_pod_cpu_usage{k8s_namespace_name=\"$namespace\",sandbox_role=\"sandbox-pod\"}) or vector(0)", + "refId": "A" + } + ] + }, + { + "id": 3, + "type": "timeseries", + "title": "Egress rate by org (bytes/sec)", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, + "fieldConfig": { "defaults": { "unit": "Bps" } }, + "targets": [ + { + "expr": "sum by (org_id) (rate(k8s_pod_network_io{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[5m]))", + "legendFormat": "{{org_id}}", + "refId": "A" + } + ] + }, + { + "id": 4, + "type": "timeseries", + "title": "CPU usage by org (cores)", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "fieldConfig": { "defaults": { "unit": "none" } }, + "targets": [ + { + "expr": "sum by (org_id) (k8s_pod_cpu_usage{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "legendFormat": "{{org_id}}", + "refId": "A" + } + ] + }, + { + "id": 5, + "type": "timeseries", + "title": "Memory (working set) by org", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "fieldConfig": { "defaults": { "unit": "bytes" } }, + "targets": [ + { + "expr": "sum by (org_id) (k8s_pod_memory_working_set_bytes{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "legendFormat": "{{org_id}}", + "refId": "A" + } + ] + }, + { + "id": 6, + "type": "table", + "title": "Top 10 sandboxes by egress (last 1h, MB)", + "gridPos": { "h": 9, "w": 24, "x": 0, "y": 16 }, + "fieldConfig": { "defaults": { "unit": "decmbytes" } }, + "options": { "showHeader": true }, + "targets": [ + { + "expr": "topk(10, sum by (org_id, user_id, sandbox_handle) (increase(k8s_pod_network_io{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[1h])) / 1024 / 1024)", + "format": "table", + "instant": true, + "refId": "A" + } + ] + } + ] +} diff --git a/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml b/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml new file mode 100644 index 0000000000..1d3a9fb267 --- /dev/null +++ b/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml @@ -0,0 +1,59 @@ +# kube-prometheus-stack overrides for the local kind cluster. +# +# Scope: just Prometheus + Grafana + the operator (so ServiceMonitor CRs work). +# Disabled the parts we don't need for sandbox-cost dashboards: +# - alertmanager: nothing to alert on yet +# - node-exporter: per-node OS metrics aren't on the customer bill +# - kube-state-metrics: the OTel collector pipeline already enriches with +# pod labels, so we don't need KSM's parallel `kube_pod_labels` series. +# Re-enable later if we want pod-state info we can't get from kubeletstats. +# +# Prod swap: keep this same shape; replace the bundled Prometheus with Mimir +# remote-write or VictoriaMetrics. The collector + dashboards stay identical. + +grafana: + # Local dev only. Prod uses SSO via the cluster's identity provider. + adminPassword: admin + defaultDashboardsTimezone: browser + service: + type: ClusterIP + # Sidecar watches for ConfigMaps with `grafana_dashboard=1` and auto-imports + # them as dashboards. up.sh creates one for sandbox-overview.json. + sidecar: + dashboards: + enabled: true + label: grafana_dashboard + labelValue: "1" + searchNamespace: ALL + # Datasource is auto-wired to the bundled Prometheus. + +prometheus: + prometheusSpec: + # Pick up ServiceMonitors created by the OTel collector chart, regardless + # of helm-managed labels. Without these, the operator only matches its own + # release's monitors and our collector's monitor is invisible. + serviceMonitorSelectorNilUsesHelmValues: false + podMonitorSelectorNilUsesHelmValues: false + ruleSelectorNilUsesHelmValues: false + probeSelectorNilUsesHelmValues: false + # Local kind nodes are tiny — keep retention short and resources modest. + retention: 7d + resources: + requests: + memory: "400Mi" + cpu: "100m" + limits: + memory: "1Gi" + +alertmanager: + enabled: false + +nodeExporter: + enabled: false + +kube-state-metrics: + enabled: false + +# Trim default dashboards/rules — we only care about sandbox-overview here. +defaultRules: + create: false diff --git a/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml b/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml new file mode 100644 index 0000000000..039faf7939 --- /dev/null +++ b/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml @@ -0,0 +1,157 @@ +# OpenTelemetry Collector (DaemonSet) — scrapes per-node kubelet for +# cAdvisor/pod metrics, enriches with our tenant labels, exposes +# /metrics for kube-prometheus-stack's Prometheus to scrape. +# +# DaemonSet (not Deployment) because kubeletstats reads from each node's +# kubelet at https://${K8S_NODE_IP}:10250 — needs one collector per node. +# +# Why not the existing mesh OTel collector subchart: that one is a +# Deployment, scoped to mesh app traces/logs/metrics (OTLP receiver). +# Different pipeline, different topology, different concern. + +mode: daemonset + +image: + # contrib has kubeletstats + k8sattributes; the core image doesn't. + repository: otel/opentelemetry-collector-contrib + +# Downward API: kubeletstats needs the host IP, k8sattributes' pod_association +# needs the pod's namespace. Auto-injected by the chart's `presets.kubeletMetrics` +# but we set explicit config below, so wire env vars directly. +extraEnvs: + - name: K8S_NODE_IP + valueFrom: + fieldRef: + fieldPath: status.hostIP + - name: K8S_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + +# RBAC: kubeletstats hits nodes/stats + nodes/proxy on the local kubelet; +# k8sattributes watches pods + namespaces cluster-wide to enrich on label +# lookup. Replicas-set/deployment perms are not needed since we filter to +# the agent-sandbox-system namespace. +clusterRole: + create: true + rules: + - apiGroups: [""] + resources: ["nodes/stats", "nodes/proxy"] + verbs: ["get"] + - apiGroups: [""] + resources: ["pods", "namespaces", "nodes"] + verbs: ["get", "list", "watch"] + +# Container-level :8889/metrics for Prometheus to scrape. +ports: + metrics: + enabled: true + containerPort: 8889 + servicePort: 8889 + protocol: TCP + +# Auto-creates a ServiceMonitor that kube-prometheus-stack's Prometheus +# (with serviceMonitorSelectorNilUsesHelmValues=false) picks up. +serviceMonitor: + enabled: true + metricsEndpoints: + - port: metrics + interval: 30s + +resources: + requests: + cpu: 100m + memory: 200Mi + limits: + memory: 500Mi + +config: + receivers: + # Suppress the OTLP receivers the chart enables by default — this + # collector is metrics-only, kubeletstats-fed. + otlp: null + jaeger: null + zipkin: null + prometheus: null + kubeletstats: + collection_interval: 30s + auth_type: serviceAccount + endpoint: "https://${env:K8S_NODE_IP}:10250" + # kind nodes use kubelet self-signed certs; skip verify locally. + # Prod: rotate to a CA-signed kubelet cert and remove this. + insecure_skip_verify: true + # `volume` group requires the PV CSI metrics endpoint which kind + # doesn't expose. node/pod/container is what we need anyway. + metric_groups: [node, pod, container] + + processors: + # Resource enrichment: read the pod's labels via the API server and + # surface them as resource attributes. resource_to_telemetry_conversion + # on the prometheus exporter then promotes them to series labels. + k8sattributes: + auth_type: serviceAccount + passthrough: false + filter: + # Watch all namespaces (otherwise we miss the kube-system pods our + # node metrics are tagged against). Filtering happens at the + # exporter/dashboard layer, not here. + node_from_env_var: K8S_NODE_NAME + pod_association: + - sources: + - from: resource_attribute + name: k8s.pod.name + - from: resource_attribute + name: k8s.namespace.name + extract: + metadata: + - k8s.pod.name + - k8s.pod.uid + - k8s.namespace.name + - k8s.node.name + - k8s.deployment.name + labels: + - tag_name: org_id + key: mesh.decocms.com/org-id + from: pod + - tag_name: user_id + key: mesh.decocms.com/user-id + from: pod + - tag_name: sandbox_handle + key: mesh.decocms.com/sandbox-handle + from: pod + - tag_name: sandbox_role + key: mesh.decocms.com/role + from: pod + batch: + send_batch_size: 1024 + timeout: 10s + memory_limiter: + check_interval: 1s + limit_percentage: 80 + spike_limit_percentage: 25 + + exporters: + debug: null + # Promotes resource attributes (org_id, user_id, sandbox_handle, …) to + # Prometheus series labels. Without this they stay as metadata and + # dashboards can't filter on them. + prometheus: + endpoint: "0.0.0.0:8889" + resource_to_telemetry_conversion: + enabled: true + send_timestamps: true + enable_open_metrics: true + + service: + telemetry: + logs: + level: info + pipelines: + # Drop the chart's default traces/logs pipelines — this collector is + # metrics-only. + traces: null + logs: null + metrics: + receivers: [kubeletstats] + processors: [memory_limiter, k8sattributes, batch] + exporters: [prometheus] diff --git a/deploy/k8s-sandbox/local/sandbox-template.yaml b/deploy/k8s-sandbox/local/sandbox-template.yaml index 4b372e9ed6..ccafae5b77 100644 --- a/deploy/k8s-sandbox/local/sandbox-template.yaml +++ b/deploy/k8s-sandbox/local/sandbox-template.yaml @@ -26,10 +26,20 @@ spec: # env. Once per-claim git credentials land (later in Stage 2+), the same # mechanism carries them. envVarsInjectionPolicy: Allowed + # The CRD defaults to Managed, which makes the operator install its own + # NetworkPolicy that only allows ingress from `app: sandbox-router` pods. + # That blocks the mesh → daemon path the preview-proxy uses. Mark the + # netpol Unmanaged so a real CNI (kindnet on recent kind, Calico/Cilium + # in prod) lets mesh reach the sandbox Service on port 9000. + networkPolicyManagement: Unmanaged podTemplate: metadata: labels: app.kubernetes.io/name: mesh-sandbox + # Do NOT set `mesh.decocms.com/role` here. Operator v0.4.2+ rejects + # claims whose additionalPodMetadata defines a label key the template + # already declared, even when values differ. The runner sets + # role=claimed via additionalPodMetadata, so this key must be absent. spec: automountServiceAccountToken: false securityContext: diff --git a/deploy/k8s-sandbox/local/up.sh b/deploy/k8s-sandbox/local/up.sh index 80dacdfe44..a05f070e99 100755 --- a/deploy/k8s-sandbox/local/up.sh +++ b/deploy/k8s-sandbox/local/up.sh @@ -14,6 +14,12 @@ CLUSTER_NAME="mesh-sandbox-dev" OPERATOR_VERSION="v0.4.2" IMAGE_TAG="mesh-sandbox:local" +# Monitoring stack pins. Bumping these is fine but verify the dashboard +# queries still work (kubeletstats metric names occasionally rename across +# OTel collector contrib releases). +KUBE_PROM_STACK_VERSION="65.5.1" +OTEL_COLLECTOR_VERSION="0.108.0" + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" SANDBOX_PKG="${REPO_ROOT}/packages/sandbox" @@ -68,4 +74,53 @@ kind load docker-image "${IMAGE_TAG}" --name "${CLUSTER_NAME}" log "applying SandboxTemplate" kubectl --context "${KCTX}" apply -f "${SCRIPT_DIR}/sandbox-template.yaml" +# 8. monitoring stack: kube-prometheus-stack (Prom + Grafana + the operator +# whose CRDs the OTel collector's ServiceMonitor depends on) followed by +# the OTel daemonset that scrapes kubelet → enriches with tenant labels → +# exposes /metrics for Prometheus to scrape. +# +# Helm enters the local stack only for these third-party charts; SandboxTemplate +# and operator stay raw kubectl. Skip via `MONITORING=0 ./up.sh` if you want +# the cluster without dashboards. +if [[ "${MONITORING:-1}" == "1" ]]; then + if ! command -v helm >/dev/null 2>&1; then + log "helm not installed; skipping monitoring stack (set MONITORING=0 to silence)" + else + log "adding helm repos" + helm repo add prometheus-community https://prometheus-community.github.io/helm-charts >/dev/null 2>&1 || true + helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts >/dev/null 2>&1 || true + helm repo update >/dev/null + + log "installing kube-prometheus-stack ${KUBE_PROM_STACK_VERSION}" + helm upgrade --install kube-prometheus-stack \ + prometheus-community/kube-prometheus-stack \ + --kube-context "${KCTX}" \ + --namespace monitoring --create-namespace \ + --version "${KUBE_PROM_STACK_VERSION}" \ + -f "${SCRIPT_DIR}/monitoring/values-kube-prometheus-stack.yaml" \ + --wait --timeout 5m + + log "installing opentelemetry-collector ${OTEL_COLLECTOR_VERSION} (daemonset)" + helm upgrade --install otel-collector-sandbox \ + open-telemetry/opentelemetry-collector \ + --kube-context "${KCTX}" \ + --namespace monitoring \ + --version "${OTEL_COLLECTOR_VERSION}" \ + -f "${SCRIPT_DIR}/monitoring/values-otel-collector.yaml" \ + --wait --timeout 3m + + log "applying sandbox dashboard ConfigMap" + # `--dry-run | apply` so re-runs replace the ConfigMap idempotently. + kubectl --context "${KCTX}" -n monitoring create configmap mesh-sandbox-dashboard \ + --from-file="${SCRIPT_DIR}/monitoring/dashboards/sandbox-overview.json" \ + --dry-run=client -o yaml | \ + kubectl --context "${KCTX}" apply -f - + kubectl --context "${KCTX}" -n monitoring label configmap mesh-sandbox-dashboard \ + grafana_dashboard=1 --overwrite >/dev/null + + log "monitoring ready: kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3001:80" + log " → http://localhost:3001 (admin / admin) → Dashboards → 'Mesh Sandbox Overview'" + fi +fi + log "ready. smoke test: see README.md" diff --git a/packages/sandbox/server/runner/k8s/client.ts b/packages/sandbox/server/runner/k8s/client.ts index 3289b9f69c..62f109a207 100644 --- a/packages/sandbox/server/runner/k8s/client.ts +++ b/packages/sandbox/server/runner/k8s/client.ts @@ -51,6 +51,15 @@ export interface SandboxClaim { spec: { sandboxTemplateRef: { name: string }; env?: SandboxClaimEnvVar[]; + /** + * Pod-level metadata the operator merges onto the spawned Pod (CRD field, + * see sandboxclaims.extensions.agents.x-k8s.io v1alpha1). Used to attach + * tenant labels for downstream metrics attribution. + */ + additionalPodMetadata?: { + labels?: Record; + annotations?: Record; + }; /** * `"none"` forces a fresh pod per claim — required when `spec.env` is * set because the operator rejects custom env when the claim would diff --git a/packages/sandbox/server/runner/k8s/index.ts b/packages/sandbox/server/runner/k8s/index.ts index 3e63c7c75e..f4d0253b06 100644 --- a/packages/sandbox/server/runner/k8s/index.ts +++ b/packages/sandbox/server/runner/k8s/index.ts @@ -15,5 +15,5 @@ export type { SandboxResource, WaitForSandboxReadyResult, } from "./client"; -export { KubernetesSandboxRunner } from "./runner"; +export { KubernetesSandboxRunner, HANDLE_PREFIX } from "./runner"; export type { KubernetesRunnerOptions } from "./runner"; diff --git a/packages/sandbox/server/runner/k8s/runner.ts b/packages/sandbox/server/runner/k8s/runner.ts index b30009f0d9..b9c7da9f6b 100644 --- a/packages/sandbox/server/runner/k8s/runner.ts +++ b/packages/sandbox/server/runner/k8s/runner.ts @@ -91,9 +91,44 @@ const DAEMON_TOKEN_BYTES = 32; const DEFAULT_IDLE_TTL_MS = 15 * 60 * 1000; /** Handle prefix + 16-hex hash = 24 chars, well under K8s's 63-char label cap. */ -const HANDLE_PREFIX = "mesh-sb-"; +export const HANDLE_PREFIX = "mesh-sb-"; const HANDLE_HASH_LEN = 16; +/** + * Headers stripped before re-issuing the preview proxy fetch. Hop-by-hop per + * RFC 7230 + cookies (preview is per-handle, not per-user — no callee session + * leak) + accept-encoding (Bun fetch auto-decompresses, so a downstream + * content-encoding would mismatch the actual body). + */ +const PREVIEW_STRIP_REQUEST_HEADERS = [ + "cookie", + "host", + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "accept-encoding", + "content-length", + "upgrade", +]; + +/** + * Stripped from the proxied response. content-encoding/length would mismatch + * after Bun fetch auto-decompresses; CSP/X-Frame-Options the daemon already + * rewrote — re-passing them defeats the iframe-embedding fix the daemon + * installed. + */ +const PREVIEW_STRIP_RESPONSE_HEADERS = [ + "connection", + "keep-alive", + "transfer-encoding", + "content-encoding", + "content-length", +]; + // Deterministic local-port range for port-forward listeners. Same // (handle, containerPort) pair → same host port across mesh restarts, so // `previewUrl` cached in the thread's vmMap stays valid when the mesh @@ -250,6 +285,108 @@ export class KubernetesSandboxRunner implements SandboxRunner { return proxyDaemonRequest(rec.daemonUrl, rec.token, path, init); } + /** + * Resolves the HTTP base URL for a sandbox's daemon. Used by the preview + * reverse-proxy at the mesh edge. + * + * Two modes: + * 1. `previewUrlPattern` set (Stage 3 / in-cluster mesh): synthesize the + * in-cluster Service URL straight from the handle. No record lookup, no + * port-forward, no health probe — the cluster DNS + downstream fetch + * are the source of truth. Crucially this means a cold mesh pod (or one + * that just restarted with an empty records map) still serves preview + * traffic without first having to rehydrate every claim. If the Service + * doesn't exist for that handle, the downstream fetch fails and the + * caller surfaces a 502. + * 2. `previewUrlPattern` unset (dev / mesh-outside-cluster): fall back to + * the 127.0.0.1 port-forwarder opened by `getRecord`. Returns null when + * the record can't be found or rehydrated — the caller surfaces 404. + * + * Preview must always land on port 9000 (daemon) — never 3000 (dev server) + * — because the daemon's reverse proxy strips CSP/X-Frame headers and + * injects the HMR bootstrap script that vite needs to function inside the + * studio iframe. Bypassing it breaks SSE + iframe embedding. + */ + async resolvePreviewUpstreamUrl(handle: string): Promise { + if (this.previewUrlPattern) { + return `http://${handle}.${this.namespace}.svc.cluster.local:${DAEMON_CONTAINER_PORT}`; + } + const rec = await this.getRecord(handle); + if (!rec) return null; + return rec.daemonUrl; + } + + /** + * Reverse-proxies an inbound preview HTTP request to the sandbox's daemon. + * Unauthenticated by design — preview URLs are open the same way Vercel + * preview URLs are; the *handle* is the secret. + * + * `/_decopilot_vm/*` access policy at the edge: + * - **GET** is allowed through. The daemon's `/events` SSE and `/scripts` + * are intentionally unauthenticated and CORS-enabled (`Allow-Origin: *`) + * because the studio UI consumes them cross-origin from the preview + * URL — that's the only path it has to live setup state. Stripping + * them here would break the studio UI's setup tab and SSE event feed. + * - **Non-GET** (POST/PUT/DELETE/etc) is rejected as defense-in-depth. + * The daemon enforces bearer auth on the mutating endpoints + * (read/write/edit/grep/glob/bash/exec/kill), but the only legitimate + * caller for those is mesh itself via the internal port-forward; the + * preview surface should never see them. + */ + async proxyPreviewRequest( + handle: string, + request: Request, + ): Promise { + const upstreamBase = await this.resolvePreviewUpstreamUrl(handle); + if (!upstreamBase) { + return jsonResponse(404, { error: "sandbox not found" }); + } + + const reqUrl = new URL(request.url); + const isAdminPath = + reqUrl.pathname === "/_decopilot_vm" || + reqUrl.pathname.startsWith("/_decopilot_vm/"); + if (isAdminPath && request.method !== "GET") { + return jsonResponse(404, { error: "not found" }); + } + + const target = `${upstreamBase}${reqUrl.pathname}${reqUrl.search}`; + const headers = new Headers(request.headers); + for (const h of PREVIEW_STRIP_REQUEST_HEADERS) headers.delete(h); + + const hasBody = request.method !== "GET" && request.method !== "HEAD"; + const init: RequestInit & { duplex?: string } = { + method: request.method, + headers, + body: hasBody ? request.body : undefined, + redirect: "manual", + signal: request.signal, + duplex: hasBody ? "half" : undefined, + }; + + let upstream: Response; + try { + upstream = await fetch(target, init as RequestInit); + } catch (err) { + console.warn( + `[${LOG_LABEL}] preview fetch to ${target} failed: ${err instanceof Error ? err.message : String(err)}`, + ); + return jsonResponse(502, { error: "sandbox daemon unreachable" }); + } + + const responseHeaders = new Headers(); + for (const [k, v] of upstream.headers.entries()) { + if (!PREVIEW_STRIP_RESPONSE_HEADERS.includes(k.toLowerCase())) { + responseHeaders.set(k, v); + } + } + return new Response(upstream.body, { + status: upstream.status, + statusText: upstream.statusText, + headers: responseHeaders, + }); + } + // ---- Ensure flow ---------------------------------------------------------- private async ensureLocked( @@ -390,6 +527,14 @@ export class KubernetesSandboxRunner implements SandboxRunner { }, spec: { sandboxTemplateRef: { name: this.sandboxTemplateName }, + // additionalPodMetadata.labels is the operator's pod-label propagation + // hook (CRD field, not a generic patch). Tenant labels here flow to + // the pod and become joinable in cAdvisor/kubelet metrics. `role` + // distinguishes claimed pods from warm-pool pods (template sets + // role=sandbox-pod by default). + additionalPodMetadata: { + labels: buildTenantPodLabels(handle, opts.tenant), + }, // `valueFrom.secretKeyRef` isn't supported on SandboxClaim env; RBAC // on the namespace is the secrecy boundary. Warm-pool off because the // operator rejects custom env on warm-pooled claims. @@ -766,6 +911,58 @@ function deterministicLocalPort(handle: string, containerPort: number): number { return PORT_RANGE_START + (hash.readUInt32BE(0) % PORT_RANGE_SIZE); } +// CORS headers on synthesized preview-proxy responses. The studio iframe +// renders under the studio origin and fetches the preview origin cross-site +// (SSE at `/_decopilot_vm/events`, plus the EventSource probeMissing fetch); +// without ACAO the browser blocks the response *and* hides the actual status, +// so a 404 from us looks like an opaque CORS failure in devtools. The daemon +// already sets ACAO on its own responses — these headers only fire on errors +// we synthesize before reaching the daemon. +function jsonResponse(status: number, body: unknown): Response { + return new Response(JSON.stringify(body), { + status, + headers: { + "content-type": "application/json", + "access-control-allow-origin": "*", + }, + }); +} + +// K8s label values: ≤63 chars, must match `(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?`. +// Org/user IDs are UUIDs in mesh and pass through unchanged; the regex check +// + truncation is defensive against future ID-shape changes (the operator will +// reject the claim outright if a label value is invalid). +const LABEL_VALUE_RE = /^([A-Za-z0-9]([-A-Za-z0-9_.]*[A-Za-z0-9])?)?$/; +const MAX_LABEL_VALUE_LEN = 63; + +function sanitizeLabelValue(value: string): string { + const truncated = value.slice(0, MAX_LABEL_VALUE_LEN); + return LABEL_VALUE_RE.test(truncated) ? truncated : ""; +} + +/** + * Pod labels for cost attribution. `role=claimed` distinguishes these from + * warm-pool pods (template sets `role=sandbox-pod` as the default). Sandbox + * handle is included so dashboards can drill from per-org aggregates down to + * a specific sandbox without needing a join table. + */ +function buildTenantPodLabels( + handle: string, + tenant: EnsureOptions["tenant"], +): Record { + const labels: Record = { + "mesh.decocms.com/role": "claimed", + "mesh.decocms.com/sandbox-handle": handle, + }; + if (tenant) { + const orgId = sanitizeLabelValue(tenant.orgId); + const userId = sanitizeLabelValue(tenant.userId); + if (orgId) labels["mesh.decocms.com/org-id"] = orgId; + if (userId) labels["mesh.decocms.com/user-id"] = userId; + } + return labels; +} + /** Fallback for when callers don't provide `repo.displayName`. */ function deriveRepoLabel(cloneUrl: string): string { try { diff --git a/packages/sandbox/server/runner/types.ts b/packages/sandbox/server/runner/types.ts index 7f191d0b7a..d02cb4d6aa 100644 --- a/packages/sandbox/server/runner/types.ts +++ b/packages/sandbox/server/runner/types.ts @@ -48,6 +48,17 @@ export interface EnsureOptions { workload?: Workload; /** Frozen for the sandbox's lifetime — changing requires recreate. */ env?: Record; + /** + * Tenant identity for cost attribution. Runners MAY surface these as + * platform-native metadata (k8s pod labels, Docker container labels) so + * downstream metrics pipelines can attribute resource usage to the owning + * org/user. Optional — callers without an org context (smoke tests, internal + * tool sandboxes) leave it unset and pods get only platform-level labels. + */ + tenant?: { + orgId: string; + userId: string; + }; } export interface ExecInput { From 3eb7005e7940026010966cf48fe3bbd2f430febd Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 12:19:32 -0300 Subject: [PATCH 11/16] Refactor monitoring configurations for Kubernetes - Updated `values-kube-prometheus-stack.yaml` to use camelCase for `kubeStateMetrics` and added comments for clarity on subchart toggles. - Modified `values-otel-collector.yaml` to change `serviceMonitor` to `podMonitor`, reflecting the new scraping strategy, and added comments regarding omitted metadata. - Adjusted `sandbox-overview.json` to replace deprecated metrics expressions with updated ones for CPU and network utilization, ensuring accurate monitoring data. These changes enhance the clarity and functionality of the monitoring setup in the Kubernetes sandbox environment. --- .../monitoring/dashboards/sandbox-overview.json | 10 +++++----- .../monitoring/values-kube-prometheus-stack.yaml | 5 ++++- .../local/monitoring/values-otel-collector.yaml | 16 ++++++++++++---- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json b/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json index 1a61a2b874..a972f4b785 100644 --- a/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json +++ b/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json @@ -34,7 +34,7 @@ }, "targets": [ { - "expr": "count by (org_id) (k8s_pod_cpu_usage{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "expr": "count by (org_id) (k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", "legendFormat": "{{org_id}}", "refId": "A" } @@ -48,7 +48,7 @@ "fieldConfig": { "defaults": { "unit": "none" } }, "targets": [ { - "expr": "count(k8s_pod_cpu_usage{k8s_namespace_name=\"$namespace\",sandbox_role=\"sandbox-pod\"}) or vector(0)", + "expr": "count(k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",app_name=\"mesh-sandbox\",sandbox_handle=\"\"}) or vector(0)", "refId": "A" } ] @@ -61,7 +61,7 @@ "fieldConfig": { "defaults": { "unit": "Bps" } }, "targets": [ { - "expr": "sum by (org_id) (rate(k8s_pod_network_io{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[5m]))", + "expr": "sum by (org_id) (rate(k8s_pod_network_io_bytes_total{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[5m]))", "legendFormat": "{{org_id}}", "refId": "A" } @@ -75,7 +75,7 @@ "fieldConfig": { "defaults": { "unit": "none" } }, "targets": [ { - "expr": "sum by (org_id) (k8s_pod_cpu_usage{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "expr": "sum by (org_id) (k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", "legendFormat": "{{org_id}}", "refId": "A" } @@ -104,7 +104,7 @@ "options": { "showHeader": true }, "targets": [ { - "expr": "topk(10, sum by (org_id, user_id, sandbox_handle) (increase(k8s_pod_network_io{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[1h])) / 1024 / 1024)", + "expr": "topk(10, sum by (org_id, user_id, sandbox_handle) (increase(k8s_pod_network_io_bytes_total{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[1h])) / 1024 / 1024)", "format": "table", "instant": true, "refId": "A" diff --git a/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml b/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml index 1d3a9fb267..c1277212d8 100644 --- a/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml +++ b/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml @@ -48,10 +48,13 @@ prometheus: alertmanager: enabled: false +# camelCase: kube-prometheus-stack's own toggles for the bundled subcharts. +# (The dashed-name forms `nodeExporter:` and `kube-state-metrics:` would be +# subchart values, not the parent toggle — silently ignored.) nodeExporter: enabled: false -kube-state-metrics: +kubeStateMetrics: enabled: false # Trim default dashboards/rules — we only care about sandbox-overview here. diff --git a/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml b/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml index 039faf7939..3e230fddde 100644 --- a/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml +++ b/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml @@ -50,9 +50,9 @@ ports: servicePort: 8889 protocol: TCP -# Auto-creates a ServiceMonitor that kube-prometheus-stack's Prometheus -# (with serviceMonitorSelectorNilUsesHelmValues=false) picks up. -serviceMonitor: +# DaemonSet mode: no Service is created (pods are per-node), so ServiceMonitor +# has nothing to scrape. PodMonitor matches pods directly. +podMonitor: enabled: true metricsEndpoints: - port: metrics @@ -103,12 +103,13 @@ config: - from: resource_attribute name: k8s.namespace.name extract: + # k8s.deployment.name intentionally omitted — would require watching + # ReplicaSets cluster-wide, and we don't use the value. metadata: - k8s.pod.name - k8s.pod.uid - k8s.namespace.name - k8s.node.name - - k8s.deployment.name labels: - tag_name: org_id key: mesh.decocms.com/org-id @@ -122,6 +123,13 @@ config: - tag_name: sandbox_role key: mesh.decocms.com/role from: pod + # `app.kubernetes.io/name=mesh-sandbox` is set by the SandboxTemplate + # on every sandbox pod (warm-pool + claimed). Lets the warm-pool + # query distinguish sandbox pods from operator/other pods in the + # namespace by app_name + absence-of-handle. + - tag_name: app_name + key: app.kubernetes.io/name + from: pod batch: send_batch_size: 1024 timeout: 10s From 3df90f74f1a70655f25e8439fa4f36f868e532c4 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 13:12:50 -0300 Subject: [PATCH 12/16] Update dependencies and enhance sandbox monitoring configurations - Bumped versions for `decocms` to 2.281.2 and `@decocms/runtime` to 1.6.0 in `bun.lock`. - Added `@opentelemetry/api` as a new dependency in the sandbox package. - Introduced new monitoring configurations for Kubernetes, including updated values for Prometheus and OpenTelemetry collector, and added a new dashboard for sandbox overview. - Improved WebSocket handling in the sandbox to manage pending frames and prevent memory exhaustion. These changes aim to enhance the observability and performance of the sandbox environment while ensuring accurate monitoring and dependency management. --- .claude/scheduled_tasks.lock | 1 + apps/mesh/src/sandbox/lifecycle.ts | 50 +- apps/mesh/src/sandbox/preview-proxy.test.ts | 48 ++ apps/mesh/src/sandbox/preview-proxy.ts | 63 ++- bun.lock | 5 +- deploy/k8s-sandbox/local/README.md | 21 +- .../dashboards/sandbox-overview.json | 115 ---- .../values-kube-prometheus-stack.local.yaml | 27 + .../values-otel-collector.local.yaml | 13 + deploy/k8s-sandbox/local/up.sh | 11 +- deploy/k8s-sandbox/monitoring/README.md | 152 ++++++ .../dashboards/sandbox-overview.json | 225 ++++++++ .../values-kube-prometheus-stack.yaml | 35 +- .../monitoring/values-otel-collector.yaml | 10 +- packages/sandbox/daemon/ws-proxy.ts | 22 +- packages/sandbox/package.json | 3 +- packages/sandbox/server/runner/k8s/client.ts | 137 +++-- packages/sandbox/server/runner/k8s/runner.ts | 505 +++++++++++++----- 18 files changed, 1070 insertions(+), 373 deletions(-) create mode 100644 .claude/scheduled_tasks.lock delete mode 100644 deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json create mode 100644 deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.local.yaml create mode 100644 deploy/k8s-sandbox/local/monitoring/values-otel-collector.local.yaml create mode 100644 deploy/k8s-sandbox/monitoring/README.md create mode 100644 deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json rename deploy/k8s-sandbox/{local => }/monitoring/values-kube-prometheus-stack.yaml (53%) rename deploy/k8s-sandbox/{local => }/monitoring/values-otel-collector.yaml (92%) diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000000..40db5a12f4 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"81d7daa7-d78f-44e2-8618-ad6a048542d9","pid":31553,"procStart":"Tue Apr 28 04:50:17 2026","acquiredAt":1777356837679} \ No newline at end of file diff --git a/apps/mesh/src/sandbox/lifecycle.ts b/apps/mesh/src/sandbox/lifecycle.ts index 09efd701f6..660c19b2af 100644 --- a/apps/mesh/src/sandbox/lifecycle.ts +++ b/apps/mesh/src/sandbox/lifecycle.ts @@ -16,10 +16,37 @@ import { } from "@decocms/sandbox/runner"; import { getDb } from "@/database"; import type { Kysely } from "kysely"; +import { meter } from "@/observability"; import type { Database as DatabaseSchema } from "@/storage/types"; import { KyselySandboxRunnerStateStore } from "@/storage/sandbox-runner-state"; const runners: Partial> = {}; +// In-flight instantiate() promises, memoized per kind. Two concurrent +// callers on a cold mesh would otherwise both miss the resolved-runner +// cache and both call instantiate(); memoizing the promise (and only +// promoting to `runners` once it resolves) collapses them to a single +// build. Cleared on failure so a retry can take a fresh swing. +const inflight: Partial>> = {}; + +function resolveOnce( + kind: RunnerKind, + build: () => Promise, +): Promise { + const cached = runners[kind]; + if (cached) return Promise.resolve(cached); + const pending = inflight[kind]; + if (pending) return pending; + const promise = build() + .then((runner) => { + runners[kind] = runner; + return runner; + }) + .finally(() => { + delete inflight[kind]; + }); + inflight[kind] = promise; + return promise; +} // Set in prod (k8s/docker behind ingress) so the runner skips the local // 127.0.0.1 port-forward path and emits a URL the user's browser can @@ -53,7 +80,14 @@ async function instantiate( const { KubernetesSandboxRunner } = await import( "@decocms/sandbox/runner/k8s" ); - return new KubernetesSandboxRunner({ stateStore, previewUrlPattern }); + // `meter` is reassigned by initObservability() after sdk.start(); read + // it at runner construction (post-init) so we get the real instruments + // not the no-op evaluated at module load. + return new KubernetesSandboxRunner({ + stateStore, + previewUrlPattern, + meter, + }); } default: { const exhaustive: never = kind; @@ -67,15 +101,11 @@ export function getSharedRunner(ctx: MeshContext): Promise { } /** VM_DELETE uses this so teardown follows the entry's recorded runnerKind. */ -export async function getRunnerByKind( +export function getRunnerByKind( ctx: MeshContext, kind: RunnerKind, ): Promise { - const cached = runners[kind]; - if (cached) return cached; - const runner = await instantiate(kind, ctx.db); - runners[kind] = runner; - return runner; + return resolveOnce(kind, () => instantiate(kind, ctx.db)); } /** @@ -88,11 +118,7 @@ export async function getRunnerByKind( export async function getOrInitSharedRunner(): Promise { const kind = tryResolveRunnerKindFromEnv(); if (!kind) return null; - const cached = runners[kind]; - if (cached) return cached; - const runner = await instantiate(kind, getDb().db); - runners[kind] = runner; - return runner; + return resolveOnce(kind, () => instantiate(kind, getDb().db)); } /** diff --git a/apps/mesh/src/sandbox/preview-proxy.test.ts b/apps/mesh/src/sandbox/preview-proxy.test.ts index 59ed698b20..59ff1cb2ae 100644 --- a/apps/mesh/src/sandbox/preview-proxy.test.ts +++ b/apps/mesh/src/sandbox/preview-proxy.test.ts @@ -6,6 +6,27 @@ import { tryUpgradePreviewWs, } from "./preview-proxy"; +/** + * Inline mirror of `applyPreviewPattern` from + * `packages/sandbox/server/runner/shared/preview-url.ts` — kept here as a + * fixture so the round-trip test below has no cross-package coupling. If the + * real implementation drifts, the round-trip test will fail and force this + * mirror to update too. + */ +function applyPreviewPatternFixture(pattern: string, handle: string): string { + const base = pattern.replace(/\/+$/, ""); + if (base.includes("{handle}")) { + return `${base.replace("{handle}", handle)}/`; + } + try { + const u = new URL(base); + u.hostname = `${handle}.${u.hostname}`; + return `${u.toString()}/`; + } catch { + return `${base}/${handle}/`; + } +} + describe("parsePreviewBaseDomain", () => { it("extracts the base from {handle}-templated patterns", () => { expect(parsePreviewBaseDomain("https://{handle}.preview.decocms.com")).toBe( @@ -87,6 +108,33 @@ describe("extractHandleFromHost", () => { }); }); +describe("applyPreviewPattern <-> parse/extract round-trip", () => { + // Walks the contract that applyPreviewPattern (runner) and + // parsePreviewBaseDomain + extractHandleFromHost (preview proxy) are + // inverses. If either side ever supports a pattern shape the other doesn't + // recognize, this test catches the mismatch before it silently misroutes + // production traffic. + const handle = "mesh-sb-abc123"; + + const patterns = [ + "https://{handle}.preview.decocms.com", + "https://preview.example.com", + "https://{handle}.preview.example.com/", + "https://stage.example.com", + ]; + + for (const pattern of patterns) { + it(`round-trips: ${pattern}`, () => { + const previewUrl = applyPreviewPatternFixture(pattern, handle); + const url = new URL(previewUrl); + const baseDomain = parsePreviewBaseDomain(pattern); + expect(baseDomain).not.toBeNull(); + const recovered = extractHandleFromHost(url.host, baseDomain!); + expect(recovered).toBe(handle); + }); + } +}); + describe("tryHandlePreviewHttp", () => { const baseDomain = "preview.example.com"; diff --git a/apps/mesh/src/sandbox/preview-proxy.ts b/apps/mesh/src/sandbox/preview-proxy.ts index 7dbe5df549..86a153cbf2 100644 --- a/apps/mesh/src/sandbox/preview-proxy.ts +++ b/apps/mesh/src/sandbox/preview-proxy.ts @@ -22,6 +22,14 @@ import { type KubernetesSandboxRunner, } from "@decocms/sandbox/runner/k8s"; +/** + * Cap on frames buffered between client upgrade and upstream WS open. Vite + * HMR sends ~1 frame per file event, so 256 covers a normal cold start with + * room to spare while preventing a slow/blackholed upstream from exhausting + * mesh memory. + */ +const MAX_PENDING_FRAMES = 256; + /** * Parses the base preview hostname (e.g. `preview.decocms.com`) out of the * `MESH_SANDBOX_PREVIEW_URL_PATTERN` value. The pattern has the form @@ -222,6 +230,28 @@ export async function tryUpgradePreviewWs( return undefined; } +/** + * Idempotent shutdown for one side of the preview WS bridge. Marks the + * connection as closed (so other event listeners stop forwarding), then + * closes both client and upstream sockets — `try/catch` around each because + * Bun + the WebSocket constructor both throw on close-after-close. + */ +function closePreviewBridge( + ws: PreviewServerWebSocket, + data: PreviewWsData, + code: number, + reason: string, +): void { + if (data.closed) return; + data.closed = true; + try { + ws.close(code, reason); + } catch {} + try { + data.upstream?.close(); + } catch {} +} + /** * Bun WebSocket handler for the upgraded preview connection. Pumps frames * between the browser side (`ws`) and the upstream daemon (`ws.data.upstream`) @@ -243,10 +273,7 @@ export const previewWebSocketHandler = { console.warn( `[preview-ws] failed to dial upstream ${data.upstreamUrl}: ${err instanceof Error ? err.message : String(err)}`, ); - data.closed = true; - try { - ws.close(1011, "upstream connect failed"); - } catch {} + closePreviewBridge(ws, data, 1011, "upstream connect failed"); return; } upstream.binaryType = "arraybuffer"; @@ -263,18 +290,10 @@ export const previewWebSocketHandler = { ws.send(ev.data as string | Uint8Array | ArrayBuffer); }); upstream.addEventListener("close", (ev: CloseEvent) => { - if (data.closed) return; - data.closed = true; - try { - ws.close(ev.code || 1000, ev.reason || ""); - } catch {} + closePreviewBridge(ws, data, ev.code || 1000, ev.reason || ""); }); upstream.addEventListener("error", () => { - if (data.closed) return; - data.closed = true; - try { - ws.close(1011, "upstream error"); - } catch {} + closePreviewBridge(ws, data, 1011, "upstream error"); }); }, message( @@ -286,17 +305,21 @@ export const previewWebSocketHandler = { const upstream = data.upstream; if (upstream && upstream.readyState === WebSocket.OPEN) { upstream.send(message); - } else { - data.pending.push(message); + return; } + // Cap the pre-handshake buffer. A blackholed upstream + a chatty client + // (e.g. vite HMR firing while the daemon is still booting) would otherwise + // grow this without bound. 1011 = "internal error" per RFC 6455. + if (data.pending.length >= MAX_PENDING_FRAMES) { + closePreviewBridge(ws, data, 1011, "preview ws backlog overflow"); + return; + } + data.pending.push(message); }, close(ws: PreviewServerWebSocket) { const data = ws.data; if (!isPreviewWsData(data)) return; - data.closed = true; - try { - data.upstream?.close(); - } catch {} + closePreviewBridge(ws, data, 1000, ""); }, }; diff --git a/bun.lock b/bun.lock index dfadf91db5..9a1981bb8f 100644 --- a/bun.lock +++ b/bun.lock @@ -53,7 +53,7 @@ }, "apps/mesh": { "name": "decocms", - "version": "2.279.4", + "version": "2.281.2", "bin": { "deco": "./dist/server/cli.js", }, @@ -272,7 +272,7 @@ }, "packages/runtime": { "name": "@decocms/runtime", - "version": "1.5.0", + "version": "1.6.0", "dependencies": { "@ai-sdk/provider": "^3.0.0", "@cloudflare/workers-types": "^4.20250617.0", @@ -297,6 +297,7 @@ "version": "0.0.1", "dependencies": { "@kubernetes/client-node": "^1.4.0", + "@opentelemetry/api": "^1.9.0", }, "devDependencies": { "@types/bun": "latest", diff --git a/deploy/k8s-sandbox/local/README.md b/deploy/k8s-sandbox/local/README.md index a4fe76a8f9..31e675fea9 100644 --- a/deploy/k8s-sandbox/local/README.md +++ b/deploy/k8s-sandbox/local/README.md @@ -10,6 +10,12 @@ for upstream third-party stacks (Prometheus, Grafana, OpenTelemetry Collector) since that's their canonical install path; mesh-owned manifests (SandboxTemplate, agent-sandbox operator) stay raw `kubectl apply`. +The monitoring stack (kube-prometheus-stack + OTel collector daemonset + +sandbox dashboard) is deployed from values shared with prod — base values +live in [`../monitoring/`](../monitoring/), and `up.sh` layers the +kind-only overlay in [`monitoring/`](monitoring/) on top. See +[`../monitoring/README.md`](../monitoring/README.md) for the prod install. + ## Prereqs - [`docker`](https://docs.docker.com/engine/install/) — running @@ -85,7 +91,7 @@ kubelet (cAdvisor) ──► OTel collector daemonset │ → series labels: org_id, user_id, sandbox_handle, sandbox_role) │ - prometheus exporter on :8889 ▼ - ServiceMonitor → kube-prometheus-stack Prometheus → Grafana + PodMonitor → kube-prometheus-stack Prometheus → Grafana ``` Pod labels come from `SandboxClaim.spec.additionalPodMetadata.labels`, @@ -104,15 +110,12 @@ MONITORING_ONLY=1 ./deploy/k8s-sandbox/local/down.sh ./deploy/k8s-sandbox/local/up.sh ``` -### Production swap - -The pipeline shape carries to prod with two changes: -1. Replace bundled Prometheus with remote-write to Mimir/VictoriaMetrics - (`prometheus.prometheusSpec.remoteWrite` in the values). -2. Restrict the k8sattributes processor's `filter.namespaces` to the - sandbox namespace once you're not also debugging system pods. +### Production install -Dashboards, label names, and metric names stay identical. +The same base values + dashboard ship to prod from this repo. See +[`../monitoring/README.md`](../monitoring/README.md) for the install +commands and the prod-overlay examples (remote-write, SSO, scoped +k8sattributes filter, ServiceMonitor for the in-cluster mesh Deployment). ## Smoke test diff --git a/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json b/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json deleted file mode 100644 index a972f4b785..0000000000 --- a/deploy/k8s-sandbox/local/monitoring/dashboards/sandbox-overview.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "title": "Mesh Sandbox Overview", - "uid": "mesh-sandbox-overview", - "schemaVersion": 39, - "version": 1, - "refresh": "30s", - "time": { "from": "now-1h", "to": "now" }, - "tags": ["mesh", "sandbox"], - "templating": { - "list": [ - { - "name": "namespace", - "type": "constant", - "current": { - "value": "agent-sandbox-system", - "text": "agent-sandbox-system" - }, - "query": "agent-sandbox-system", - "hide": 2 - } - ] - }, - "panels": [ - { - "id": 1, - "type": "stat", - "title": "Active sandboxes (by org)", - "gridPos": { "h": 6, "w": 6, "x": 0, "y": 0 }, - "fieldConfig": { "defaults": { "unit": "none" } }, - "options": { - "reduceOptions": { "calcs": ["lastNotNull"] }, - "graphMode": "area", - "justifyMode": "auto" - }, - "targets": [ - { - "expr": "count by (org_id) (k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", - "legendFormat": "{{org_id}}", - "refId": "A" - } - ] - }, - { - "id": 2, - "type": "stat", - "title": "Warm-pool overhead pods (no org)", - "gridPos": { "h": 6, "w": 6, "x": 6, "y": 0 }, - "fieldConfig": { "defaults": { "unit": "none" } }, - "targets": [ - { - "expr": "count(k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",app_name=\"mesh-sandbox\",sandbox_handle=\"\"}) or vector(0)", - "refId": "A" - } - ] - }, - { - "id": 3, - "type": "timeseries", - "title": "Egress rate by org (bytes/sec)", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, - "fieldConfig": { "defaults": { "unit": "Bps" } }, - "targets": [ - { - "expr": "sum by (org_id) (rate(k8s_pod_network_io_bytes_total{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[5m]))", - "legendFormat": "{{org_id}}", - "refId": "A" - } - ] - }, - { - "id": 4, - "type": "timeseries", - "title": "CPU usage by org (cores)", - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, - "fieldConfig": { "defaults": { "unit": "none" } }, - "targets": [ - { - "expr": "sum by (org_id) (k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", - "legendFormat": "{{org_id}}", - "refId": "A" - } - ] - }, - { - "id": 5, - "type": "timeseries", - "title": "Memory (working set) by org", - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, - "fieldConfig": { "defaults": { "unit": "bytes" } }, - "targets": [ - { - "expr": "sum by (org_id) (k8s_pod_memory_working_set_bytes{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", - "legendFormat": "{{org_id}}", - "refId": "A" - } - ] - }, - { - "id": 6, - "type": "table", - "title": "Top 10 sandboxes by egress (last 1h, MB)", - "gridPos": { "h": 9, "w": 24, "x": 0, "y": 16 }, - "fieldConfig": { "defaults": { "unit": "decmbytes" } }, - "options": { "showHeader": true }, - "targets": [ - { - "expr": "topk(10, sum by (org_id, user_id, sandbox_handle) (increase(k8s_pod_network_io_bytes_total{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[1h])) / 1024 / 1024)", - "format": "table", - "instant": true, - "refId": "A" - } - ] - } - ] -} diff --git a/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.local.yaml b/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.local.yaml new file mode 100644 index 0000000000..4a52b16042 --- /dev/null +++ b/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.local.yaml @@ -0,0 +1,27 @@ +# Kind-only overlay layered on top of `../../monitoring/values-kube-prometheus-stack.yaml`. +# Helm deep-merges these values into the base — see `local/up.sh` for the +# `helm upgrade -f base.yaml -f local-overlay.yaml` invocation. +# +# Anything here is unsafe / wrong for prod. + +grafana: + # Plaintext admin password is fine for kind, never for prod. Prod sets + # `grafana.admin.existingSecret` or wires SSO. + adminPassword: admin + +prometheus: + prometheusSpec: + # Mesh runs on the host (`bun run dev`) in local-dev mode, exposing + # /metrics on http://localhost:3000. From kind's perspective the host + # is reachable via `host.docker.internal` (Docker Desktop on macOS/Windows + # provides this DNS name natively; on Linux you'd need `extraHosts`). + # Prod replaces this with a ServiceMonitor on the in-cluster mesh + # Deployment. + additionalScrapeConfigs: + - job_name: mesh-host + scrape_interval: 30s + metrics_path: /metrics + static_configs: + - targets: ["host.docker.internal:3000"] + labels: + source: mesh-runner diff --git a/deploy/k8s-sandbox/local/monitoring/values-otel-collector.local.yaml b/deploy/k8s-sandbox/local/monitoring/values-otel-collector.local.yaml new file mode 100644 index 0000000000..a79b9feb1e --- /dev/null +++ b/deploy/k8s-sandbox/local/monitoring/values-otel-collector.local.yaml @@ -0,0 +1,13 @@ +# Kind-only overlay layered on top of `../../monitoring/values-otel-collector.yaml`. +# Helm deep-merges these values into the base — see `local/up.sh` for the +# `helm upgrade -f base.yaml -f local-overlay.yaml` invocation. +# +# Anything here is unsafe / wrong for prod. + +config: + receivers: + kubeletstats: + # kind nodes use kubelet self-signed certs; skip verify locally. + # Prod relies on the cluster's CA-signed kubelet certs and never + # sets this. + insecure_skip_verify: true diff --git a/deploy/k8s-sandbox/local/up.sh b/deploy/k8s-sandbox/local/up.sh index a05f070e99..9472cbafc7 100755 --- a/deploy/k8s-sandbox/local/up.sh +++ b/deploy/k8s-sandbox/local/up.sh @@ -24,6 +24,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" SANDBOX_PKG="${REPO_ROOT}/packages/sandbox" DOCKERFILE="${SANDBOX_PKG}/image/Dockerfile" +# Shared (prod-safe) monitoring values live one level up; this script layers +# the kind-only overlay in local/monitoring/ on top. +MONITORING_DIR="${SCRIPT_DIR}/../monitoring" MANIFEST_URL="https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${OPERATOR_VERSION}/manifest.yaml" EXTENSIONS_URL="https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${OPERATOR_VERSION}/extensions.yaml" @@ -97,7 +100,8 @@ if [[ "${MONITORING:-1}" == "1" ]]; then --kube-context "${KCTX}" \ --namespace monitoring --create-namespace \ --version "${KUBE_PROM_STACK_VERSION}" \ - -f "${SCRIPT_DIR}/monitoring/values-kube-prometheus-stack.yaml" \ + -f "${MONITORING_DIR}/values-kube-prometheus-stack.yaml" \ + -f "${SCRIPT_DIR}/monitoring/values-kube-prometheus-stack.local.yaml" \ --wait --timeout 5m log "installing opentelemetry-collector ${OTEL_COLLECTOR_VERSION} (daemonset)" @@ -106,13 +110,14 @@ if [[ "${MONITORING:-1}" == "1" ]]; then --kube-context "${KCTX}" \ --namespace monitoring \ --version "${OTEL_COLLECTOR_VERSION}" \ - -f "${SCRIPT_DIR}/monitoring/values-otel-collector.yaml" \ + -f "${MONITORING_DIR}/values-otel-collector.yaml" \ + -f "${SCRIPT_DIR}/monitoring/values-otel-collector.local.yaml" \ --wait --timeout 3m log "applying sandbox dashboard ConfigMap" # `--dry-run | apply` so re-runs replace the ConfigMap idempotently. kubectl --context "${KCTX}" -n monitoring create configmap mesh-sandbox-dashboard \ - --from-file="${SCRIPT_DIR}/monitoring/dashboards/sandbox-overview.json" \ + --from-file="${MONITORING_DIR}/dashboards/sandbox-overview.json" \ --dry-run=client -o yaml | \ kubectl --context "${KCTX}" apply -f - kubectl --context "${KCTX}" -n monitoring label configmap mesh-sandbox-dashboard \ diff --git a/deploy/k8s-sandbox/monitoring/README.md b/deploy/k8s-sandbox/monitoring/README.md new file mode 100644 index 0000000000..095ac58bd2 --- /dev/null +++ b/deploy/k8s-sandbox/monitoring/README.md @@ -0,0 +1,152 @@ +# Mesh sandbox monitoring stack + +Per-org / per-sandbox cost-attribution metrics. Pipeline: + +``` +kubelet (cAdvisor) ──► OTel collector daemonset + │ - kubeletstats receiver + │ - k8sattributes processor (reads pod labels: + │ mesh.decocms.com/{org-id,user-id,sandbox-handle,role} + │ → series labels: org_id, user_id, sandbox_handle, sandbox_role) + │ - prometheus exporter on :8889 + ▼ + PodMonitor → kube-prometheus-stack Prometheus → Grafana +``` + +Pod labels are populated by `KubernetesSandboxRunner.provision()` from the +`tenant` field on `EnsureOptions` and surface on every sandbox pod via +`SandboxClaim.spec.additionalPodMetadata.labels`. + +## Files + +| File | What it is | +|---|---| +| `values-kube-prometheus-stack.yaml` | Base values — Prometheus + Grafana + the operator. Prod-safe defaults; no admin credentials, no host scrape configs. | +| `values-otel-collector.yaml` | Base values — OTel collector daemonset that scrapes kubelet → enriches with tenant labels → exposes `/metrics` for Prometheus. | +| `dashboards/sandbox-overview.json` | Grafana dashboard. Loaded via the `grafana_dashboard=1` ConfigMap label sidecar. | + +Local kind layers a kind-only overlay on top of these — see +`../local/monitoring/`. Prod layers its own overlay (storage backend, auth, +remote-write, larger resources). + +## Prod install + +Versions track [`local/up.sh`][up.sh] — bump them together. + +```bash +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts +helm repo update + +# 1. kube-prometheus-stack (Prometheus + Grafana + operator) +helm upgrade --install kube-prometheus-stack \ + prometheus-community/kube-prometheus-stack \ + --namespace monitoring --create-namespace \ + --version 65.5.1 \ + -f deploy/k8s-sandbox/monitoring/values-kube-prometheus-stack.yaml \ + -f your-prod-overlay.yaml \ + --wait + +# 2. OTel collector daemonset +helm upgrade --install otel-collector-sandbox \ + open-telemetry/opentelemetry-collector \ + --namespace monitoring \ + --version 0.108.0 \ + -f deploy/k8s-sandbox/monitoring/values-otel-collector.yaml \ + -f your-prod-overlay.yaml \ + --wait + +# 3. Grafana dashboard ConfigMap (sidecar auto-imports it) +kubectl -n monitoring create configmap mesh-sandbox-dashboard \ + --from-file=deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json \ + --dry-run=client -o yaml | kubectl apply -f - +kubectl -n monitoring label configmap mesh-sandbox-dashboard grafana_dashboard=1 --overwrite +``` + +## Prod overlay examples + +### kube-prometheus-stack + +```yaml +# your-prod-overlay.yaml +grafana: + admin: + existingSecret: grafana-admin # provision out of band + userKey: admin-user + passwordKey: admin-password + grafana.ini: + auth.generic_oauth: { ... } # SSO + +prometheus: + prometheusSpec: + retention: 15d # bump from base 7d + resources: + requests: { cpu: 500m, memory: 2Gi } + limits: { memory: 4Gi } + # Stream long-term storage to Mimir / VictoriaMetrics / Cortex + remoteWrite: + - url: https://mimir.internal/api/v1/push + # ...auth bits + # In-cluster scrape of the mesh Deployment (replaces the local + # host.docker.internal scrape). + additionalScrapeConfigs: + - job_name: mesh + kubernetes_sd_configs: + - role: endpoints + namespaces: { names: [mesh] } + relabel_configs: + - source_labels: [__meta_kubernetes_service_label_app_kubernetes_io_name] + regex: mesh + action: keep +``` + +Or, cleaner: deploy a `ServiceMonitor` alongside the mesh chart and let the +operator pick it up via the +`serviceMonitorSelectorNilUsesHelmValues: false` knob already set in the +base values. + +### OTel collector + +```yaml +# your-prod-overlay.yaml +config: + processors: + k8sattributes: + filter: + # Scope label enrichment to the sandbox namespace once you're not + # also debugging system pods. + namespaces: [agent-sandbox-system] +``` + +## Verify + +Confirm tenant labels are landing on sandbox pods: + +```bash +kubectl get pod -n agent-sandbox-system --show-labels | grep mesh.decocms.com +``` + +Confirm the collector's `/metrics` endpoint is being scraped: in the +Prometheus UI (or whatever queries your remote-write target), + +```promql +sum by (org_id, sandbox_handle) ( + rate(container_cpu_usage_seconds_total{namespace="agent-sandbox-system"}[5m]) +) +``` + +should return one series per `(org_id, sandbox_handle)` pair with active +sandboxes. + +## Versioning + +| Component | Pinned version | Where | +|---|---|---| +| `kube-prometheus-stack` | `65.5.1` | `local/up.sh` `KUBE_PROM_STACK_VERSION` | +| `opentelemetry-collector` | `0.108.0` | `local/up.sh` `OTEL_COLLECTOR_VERSION` | + +Bumping these is fine, but verify the dashboard's PromQL queries still work +— kubeletstats metric names occasionally rename across OTel collector +contrib releases. + +[up.sh]: ../local/up.sh diff --git a/deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json b/deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json new file mode 100644 index 0000000000..d184eb58bb --- /dev/null +++ b/deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json @@ -0,0 +1,225 @@ +{ + "title": "Mesh Sandbox Overview", + "uid": "mesh-sandbox-overview", + "schemaVersion": 39, + "version": 1, + "refresh": "30s", + "time": { "from": "now-1h", "to": "now" }, + "tags": ["mesh", "sandbox"], + "templating": { + "list": [ + { + "name": "namespace", + "type": "constant", + "current": { + "value": "agent-sandbox-system", + "text": "agent-sandbox-system" + }, + "query": "agent-sandbox-system", + "hide": 2 + } + ] + }, + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Active sandboxes", + "description": "Total live sandboxes (claimed pods in the namespace). Source: cAdvisor via kubeletstats — depends only on the in-cluster pipeline so it stays accurate even if mesh isn't running locally. Lags ~30s on creation, up to 5min on deletion (Prometheus staleness).", + "gridPos": { "h": 6, "w": 4, "x": 0, "y": 0 }, + "fieldConfig": { "defaults": { "unit": "none", "noValue": "0" } }, + "options": { + "reduceOptions": { "calcs": ["last"] }, + "graphMode": "none", + "justifyMode": "auto" + }, + "targets": [ + { + "expr": "count(k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"}) or vector(0)", + "refId": "A", + "instant": true, + "range": false + } + ] + }, + { + "id": 11, + "type": "stat", + "title": "Orgs with active sandboxes", + "description": "Distinct orgs currently holding at least one sandbox.", + "gridPos": { "h": 6, "w": 4, "x": 4, "y": 0 }, + "fieldConfig": { "defaults": { "unit": "none", "noValue": "0" } }, + "options": { + "reduceOptions": { "calcs": ["last"] }, + "graphMode": "none", + "justifyMode": "auto" + }, + "targets": [ + { + "expr": "count(count by (org_id) (k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})) or vector(0)", + "refId": "A", + "instant": true, + "range": false + } + ] + }, + { + "id": 2, + "type": "stat", + "title": "Warm-pool overhead pods", + "description": "Sandbox-template pods sitting idle in the namespace with no owning org (sandbox_handle empty). Cost paid for warm-start latency savings.", + "gridPos": { "h": 6, "w": 4, "x": 8, "y": 0 }, + "fieldConfig": { "defaults": { "unit": "none", "noValue": "0" } }, + "options": { + "reduceOptions": { "calcs": ["last"] }, + "graphMode": "none", + "justifyMode": "auto" + }, + "targets": [ + { + "expr": "count(k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",app_name=\"mesh-sandbox\",sandbox_handle=\"\"}) or vector(0)", + "refId": "A", + "instant": true, + "range": false + } + ] + }, + { + "id": 3, + "type": "timeseries", + "title": "Egress rate by org (bytes/sec)", + "gridPos": { "h": 6, "w": 12, "x": 12, "y": 0 }, + "fieldConfig": { "defaults": { "unit": "Bps" } }, + "targets": [ + { + "expr": "sum by (org_id) (rate(k8s_pod_network_io_bytes_total{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[5m]))", + "legendFormat": "{{org_id}}", + "refId": "A" + } + ] + }, + { + "id": 4, + "type": "timeseries", + "title": "CPU usage by org (cores)", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 }, + "fieldConfig": { "defaults": { "unit": "none" } }, + "targets": [ + { + "expr": "sum by (org_id) (k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "legendFormat": "{{org_id}}", + "refId": "A" + } + ] + }, + { + "id": 5, + "type": "timeseries", + "title": "Memory (working set) by org", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 }, + "fieldConfig": { "defaults": { "unit": "bytes" } }, + "targets": [ + { + "expr": "sum by (org_id) (k8s_pod_memory_working_set_bytes{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "legendFormat": "{{org_id}}", + "refId": "A" + } + ] + }, + { + "id": 6, + "type": "table", + "title": "Top 10 sandboxes by egress (last 1h, MB)", + "gridPos": { "h": 9, "w": 24, "x": 0, "y": 14 }, + "fieldConfig": { "defaults": { "unit": "decmbytes" } }, + "options": { "showHeader": true }, + "targets": [ + { + "expr": "topk(10, sum by (org_id, user_id, sandbox_handle) (increase(k8s_pod_network_io_bytes_total{k8s_namespace_name=\"$namespace\",direction=\"transmit\",sandbox_role=\"claimed\"}[1h])) / 1024 / 1024)", + "format": "table", + "instant": true, + "refId": "A" + } + ] + }, + { + "id": 7, + "type": "row", + "title": "Mesh-side (runner POV)", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 }, + "collapsed": false + }, + { + "id": 8, + "type": "timeseries", + "title": "Active sandboxes by org over time", + "description": "Per-org count of claimed pods. Useful for spotting which tenants are spinning up / draining. Source: cAdvisor.", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 }, + "fieldConfig": { "defaults": { "unit": "none" } }, + "targets": [ + { + "expr": "count by (org_id) (k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "legendFormat": "{{org_id}}", + "refId": "A" + } + ] + }, + { + "id": 9, + "type": "timeseries", + "title": "Ensure outcomes (cold-start ratio)", + "description": "fresh = new claim provisioned, resume = state-store rehydrate, adopt = adopted an existing cluster claim. High fresh ratio after a mesh restart = warm-pool would help.", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 }, + "fieldConfig": { "defaults": { "unit": "ops" } }, + "targets": [ + { + "expr": "sum by (outcome) (rate(mesh_sandbox_ensure_outcome_total{runner_kind=\"kubernetes\"}[5m]))", + "legendFormat": "{{outcome}}", + "refId": "A" + } + ] + }, + { + "id": 10, + "type": "timeseries", + "title": "Proxy P95 latency by source (ms)", + "description": "P95 of mesh→daemon proxy duration. source=daemon is tool exec / control plane; source=preview is iframe traffic. Spikes correlate with daemon load or port-forward stalls (dev mode).", + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 32 }, + "fieldConfig": { "defaults": { "unit": "ms" } }, + "targets": [ + { + "expr": "histogram_quantile(0.95, sum by (le, source) (rate(mesh_sandbox_proxy_duration_ms_bucket{runner_kind=\"kubernetes\"}[5m])))", + "legendFormat": "{{source}}", + "refId": "A" + } + ] + }, + { + "id": 12, + "type": "row", + "title": "Cross-checks (mesh ↔ cluster drift)", + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 40 }, + "collapsed": false + }, + { + "id": 13, + "type": "timeseries", + "title": "Active count: mesh-side vs cAdvisor", + "description": "Two views of 'how many sandboxes exist now'. mesh-side is what the mesh runner believes (real-time UpDownCounter); cAdvisor is what kubeletstats reports for pods consuming resources (lags ~30s–5min). Persistent divergence indicates orphan claims (mesh deleted but K8s didn't reap) or pods mesh doesn't know about (cluster drift).", + "gridPos": { "h": 8, "w": 24, "x": 0, "y": 41 }, + "fieldConfig": { "defaults": { "unit": "none" } }, + "targets": [ + { + "expr": "sum(mesh_sandbox_active{runner_kind=\"kubernetes\"})", + "legendFormat": "mesh", + "refId": "A" + }, + { + "expr": "count(k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",sandbox_role=\"claimed\"})", + "legendFormat": "cAdvisor", + "refId": "B" + } + ] + } + ] +} diff --git a/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml b/deploy/k8s-sandbox/monitoring/values-kube-prometheus-stack.yaml similarity index 53% rename from deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml rename to deploy/k8s-sandbox/monitoring/values-kube-prometheus-stack.yaml index c1277212d8..18b6b9ba69 100644 --- a/deploy/k8s-sandbox/local/monitoring/values-kube-prometheus-stack.yaml +++ b/deploy/k8s-sandbox/monitoring/values-kube-prometheus-stack.yaml @@ -1,24 +1,28 @@ -# kube-prometheus-stack overrides for the local kind cluster. +# kube-prometheus-stack values for the mesh sandbox monitoring stack. # -# Scope: just Prometheus + Grafana + the operator (so ServiceMonitor CRs work). -# Disabled the parts we don't need for sandbox-cost dashboards: +# Used by both local kind (`deploy/k8s-sandbox/local/up.sh`) and prod +# installs. Local layers a kind-only overlay on top — see +# `local/monitoring/values-kube-prometheus-stack.local.yaml`. +# +# Scope: Prometheus + Grafana + the prometheus-operator (so ServiceMonitor +# / PodMonitor CRs work). Disabled the parts we don't need for sandbox +# cost-attribution dashboards: # - alertmanager: nothing to alert on yet # - node-exporter: per-node OS metrics aren't on the customer bill # - kube-state-metrics: the OTel collector pipeline already enriches with # pod labels, so we don't need KSM's parallel `kube_pod_labels` series. -# Re-enable later if we want pod-state info we can't get from kubeletstats. -# -# Prod swap: keep this same shape; replace the bundled Prometheus with Mimir -# remote-write or VictoriaMetrics. The collector + dashboards stay identical. +# Re-enable later if you want pod-state info we can't get from kubeletstats. grafana: - # Local dev only. Prod uses SSO via the cluster's identity provider. - adminPassword: admin + # Prod must override `adminPassword` (or set `admin.existingSecret`) and + # typically wires SSO via grafana.ini. Local sets a plaintext password + # in its overlay. defaultDashboardsTimezone: browser service: type: ClusterIP # Sidecar watches for ConfigMaps with `grafana_dashboard=1` and auto-imports - # them as dashboards. up.sh creates one for sandbox-overview.json. + # them as dashboards. up.sh creates one for sandbox-overview.json; prod can + # apply the same ConfigMap (or commit it as a manifest). sidecar: dashboards: enabled: true @@ -29,14 +33,17 @@ grafana: prometheus: prometheusSpec: - # Pick up ServiceMonitors created by the OTel collector chart, regardless - # of helm-managed labels. Without these, the operator only matches its own - # release's monitors and our collector's monitor is invisible. + # Pick up ServiceMonitors / PodMonitors created by other charts (the OTel + # collector chart in particular) regardless of helm-managed labels. + # Without these, the operator only matches its own release's monitors + # and our collector's monitor is invisible. serviceMonitorSelectorNilUsesHelmValues: false podMonitorSelectorNilUsesHelmValues: false ruleSelectorNilUsesHelmValues: false probeSelectorNilUsesHelmValues: false - # Local kind nodes are tiny — keep retention short and resources modest. + # Modest defaults: prod typically replaces these with `remoteWrite` to + # Mimir/VictoriaMetrics and bumps `retention` + `resources`. See + # `monitoring/README.md` for prod overlay examples. retention: 7d resources: requests: diff --git a/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml b/deploy/k8s-sandbox/monitoring/values-otel-collector.yaml similarity index 92% rename from deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml rename to deploy/k8s-sandbox/monitoring/values-otel-collector.yaml index 3e230fddde..d79d2cad25 100644 --- a/deploy/k8s-sandbox/local/monitoring/values-otel-collector.yaml +++ b/deploy/k8s-sandbox/monitoring/values-otel-collector.yaml @@ -2,6 +2,10 @@ # cAdvisor/pod metrics, enriches with our tenant labels, exposes # /metrics for kube-prometheus-stack's Prometheus to scrape. # +# Used by both local kind (`deploy/k8s-sandbox/local/up.sh`) and prod +# installs. Local layers a kind-only overlay on top — see +# `local/monitoring/values-otel-collector.local.yaml`. +# # DaemonSet (not Deployment) because kubeletstats reads from each node's # kubelet at https://${K8S_NODE_IP}:10250 — needs one collector per node. # @@ -77,9 +81,9 @@ config: collection_interval: 30s auth_type: serviceAccount endpoint: "https://${env:K8S_NODE_IP}:10250" - # kind nodes use kubelet self-signed certs; skip verify locally. - # Prod: rotate to a CA-signed kubelet cert and remove this. - insecure_skip_verify: true + # Prod relies on the cluster's CA-signed kubelet certs. Local kind + # uses self-signed kubelet certs and overrides `insecure_skip_verify` + # in `local/monitoring/values-otel-collector.local.yaml`. # `volume` group requires the PV CSI metrics endpoint which kind # doesn't expose. node/pod/container is what we need anyway. metric_groups: [node, pod, container] diff --git a/packages/sandbox/daemon/ws-proxy.ts b/packages/sandbox/daemon/ws-proxy.ts index b53fe9bce6..2e605d4be9 100644 --- a/packages/sandbox/daemon/ws-proxy.ts +++ b/packages/sandbox/daemon/ws-proxy.ts @@ -15,6 +15,14 @@ */ import type { ServerWebSocket } from "bun"; +/** + * Cap on frames buffered between client upgrade and upstream WS open. The + * upstream here is the in-pod dev server on localhost; if it isn't yet + * listening (booting / crashed), an unbounded pending queue would let a + * chatty client exhaust the daemon's memory. + */ +const MAX_PENDING_FRAMES = 256; + export interface WsProxyData { /** Full upstream URL — `ws://localhost:?`. */ target: string; @@ -78,9 +86,19 @@ export function makeWsUpgrader(getDevPort: () => number) { try { upstream.send(frame as never); } catch {} - } else { - ws.data.pending.push(frame as ArrayBuffer | string); + return; + } + if (ws.data.pending.length >= MAX_PENDING_FRAMES) { + // Backlog overflow: upstream isn't draining. 1011 = internal error. + try { + ws.close(1011, "ws-proxy backlog overflow"); + } catch {} + try { + ws.data.upstream?.close(); + } catch {} + return; } + ws.data.pending.push(frame as ArrayBuffer | string); }, close(ws: ServerWebSocket): void { diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 0e8e32fa12..da613e79fb 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -16,7 +16,8 @@ "./runner/freestyle": "./server/runner/freestyle/index.ts" }, "dependencies": { - "@kubernetes/client-node": "^1.4.0" + "@kubernetes/client-node": "^1.4.0", + "@opentelemetry/api": "^1.9.0" }, "optionalDependencies": { "@freestyle-sh/with-bun": "^0.2.12", diff --git a/packages/sandbox/server/runner/k8s/client.ts b/packages/sandbox/server/runner/k8s/client.ts index 62f109a207..6cc68890a1 100644 --- a/packages/sandbox/server/runner/k8s/client.ts +++ b/packages/sandbox/server/runner/k8s/client.ts @@ -166,12 +166,17 @@ function bufferLike(v: unknown): string | undefined { } interface KubeFetchInit { - method: "GET" | "POST" | "DELETE"; + method: "GET" | "POST" | "DELETE" | "PATCH"; path: string; body?: unknown; signal?: AbortSignal; /** Extra Accept / query hints. Merged with auth headers. */ headers?: Record; + /** + * Required iff `method === "PATCH"`. Drives the patch content-type: + * RFC 7396 merge-patch (CRDs) vs. strategic-merge (built-in types). + */ + patchType?: "merge" | "strategic-merge"; } /** @@ -184,9 +189,13 @@ async function kubeFetch( init: KubeFetchInit, ): Promise { const auth = await resolveKubeAuth(kc); - const url = `${auth.server}${init.path}`; const headers: Record = { ...auth.headers, ...init.headers }; - if (init.body !== undefined && !("content-type" in headers)) { + if (init.method === "PATCH") { + headers["content-type"] = + init.patchType === "strategic-merge" + ? "application/strategic-merge-patch+json" + : "application/merge-patch+json"; + } else if (init.body !== undefined && !("content-type" in headers)) { headers["content-type"] = "application/json"; } const reqInit: RequestInit & { tls?: typeof auth.tls } = { @@ -196,32 +205,7 @@ async function kubeFetch( signal: init.signal, tls: auth.tls, }; - return fetch(url, reqInit as RequestInit); -} - -interface KubeFetchInitWithPatch extends Omit { - method: "PATCH"; - patchType: "merge" | "strategic-merge"; -} - -async function kubePatch( - kc: KubeConfig, - init: KubeFetchInitWithPatch, -): Promise { - const auth = await resolveKubeAuth(kc); - const url = `${auth.server}${init.path}`; - const contentType = - init.patchType === "strategic-merge" - ? "application/strategic-merge-patch+json" - : "application/merge-patch+json"; - const reqInit: RequestInit & { tls?: typeof auth.tls } = { - method: "PATCH", - headers: { ...auth.headers, "content-type": contentType }, - body: init.body === undefined ? undefined : JSON.stringify(init.body), - signal: init.signal, - tls: auth.tls, - }; - return fetch(url, reqInit as RequestInit); + return fetch(`${auth.server}${init.path}`, reqInit as RequestInit); } /** HTTP error carrier used for the 404 fast-path before SandboxError wrapping. */ @@ -252,6 +236,31 @@ async function ensureOk(resp: Response, action: string): Promise { throw new KubeHttpError(resp.status, body, message); } +/** + * Issue a kube call where 404 is *not* an error (the resource was already + * gone; mesh's next ensure() recreates it). On 404, returns `null`. On 2xx, + * returns the parsed JSON body — or `null` for callers that don't need it. + * All other errors are wrapped in `SandboxError` with `wrapMessage` as the + * surfaced label. + */ +async function callSwallowing404( + kc: KubeConfig, + init: KubeFetchInit, + action: string, + wrapMessage: string, + parse: "json" | "none" = "none", +): Promise { + try { + const resp = await kubeFetch(kc, init); + if (resp.status === 404) return null; + await ensureOk(resp, action); + if (parse === "json") return (await resp.json()) as T; + return null; + } catch (error) { + throw new SandboxError(wrapMessage, error); + } +} + // ---- Public surface --------------------------------------------------------- const CLAIM_PATH_PREFIX = `/apis/${K8S_CONSTANTS.CLAIM_API_GROUP}/${K8S_CONSTANTS.CLAIM_API_VERSION}/namespaces`; @@ -273,6 +282,10 @@ export async function createSandboxClaim( } } +function claimPath(namespace: string, claimName: string): string { + return `${CLAIM_PATH_PREFIX}/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.CLAIM_PLURAL}/${encodeURIComponent(claimName)}`; +} + /** * Update the claim's idle-reap clock. The agent-sandbox operator honors * `spec.lifecycle.shutdownTime` with `shutdownPolicy: Delete`: once the @@ -294,30 +307,19 @@ export async function patchSandboxClaimShutdown( claimName: string, shutdownTime: string, ): Promise { - const path = `${CLAIM_PATH_PREFIX}/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.CLAIM_PLURAL}/${encodeURIComponent(claimName)}`; - try { - const resp = await kubePatch(kc, { + await callSwallowing404( + kc, + { method: "PATCH", - path, + path: claimPath(namespace, claimName), patchType: "merge", body: { - spec: { - lifecycle: { - shutdownPolicy: "Delete", - shutdownTime, - }, - }, + spec: { lifecycle: { shutdownPolicy: "Delete", shutdownTime } }, }, - }); - if (resp.status === 404) return; - await ensureOk(resp, "patchSandboxClaimShutdown"); - } catch (error) { - if (error instanceof KubeHttpError && error.status === 404) return; - throw new SandboxError( - `Failed to patch SandboxClaim shutdownTime: ${claimName}`, - error, - ); - } + }, + "patchSandboxClaimShutdown", + `Failed to patch SandboxClaim shutdownTime: ${claimName}`, + ); } export async function deleteSandboxClaim( @@ -325,18 +327,12 @@ export async function deleteSandboxClaim( namespace: string, claimName: string, ): Promise { - const path = `${CLAIM_PATH_PREFIX}/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.CLAIM_PLURAL}/${encodeURIComponent(claimName)}`; - try { - const resp = await kubeFetch(kc, { method: "DELETE", path }); - if (resp.status === 404) return; - await ensureOk(resp, "deleteSandboxClaim"); - } catch (error) { - if (error instanceof KubeHttpError && error.status === 404) return; - throw new SandboxError( - `Failed to delete SandboxClaim: ${claimName}`, - error, - ); - } + await callSwallowing404( + kc, + { method: "DELETE", path: claimPath(namespace, claimName) }, + "deleteSandboxClaim", + `Failed to delete SandboxClaim: ${claimName}`, + ); } export async function getSandboxClaim( @@ -344,17 +340,14 @@ export async function getSandboxClaim( namespace: string, claimName: string, ): Promise { - const path = `${CLAIM_PATH_PREFIX}/${encodeURIComponent(namespace)}/${K8S_CONSTANTS.CLAIM_PLURAL}/${encodeURIComponent(claimName)}`; - try { - const resp = await kubeFetch(kc, { method: "GET", path }); - if (resp.status === 404) return undefined; - await ensureOk(resp, "getSandboxClaim"); - return (await resp.json()) as SandboxResource; - } catch (error) { - if (error instanceof KubeHttpError && error.status === 404) - return undefined; - throw new SandboxError(`Failed to get SandboxClaim: ${claimName}`, error); - } + const found = await callSwallowing404( + kc, + { method: "GET", path: claimPath(namespace, claimName) }, + "getSandboxClaim", + `Failed to get SandboxClaim: ${claimName}`, + "json", + ); + return found ?? undefined; } export interface WaitForSandboxReadyResult { diff --git a/packages/sandbox/server/runner/k8s/runner.ts b/packages/sandbox/server/runner/k8s/runner.ts index b9c7da9f6b..20695c5d13 100644 --- a/packages/sandbox/server/runner/k8s/runner.ts +++ b/packages/sandbox/server/runner/k8s/runner.ts @@ -31,6 +31,12 @@ import { KubeConfig as KubeConfigClass, PortForward, } from "@kubernetes/client-node"; +import type { + Counter, + Histogram, + Meter, + UpDownCounter, +} from "@opentelemetry/api"; import { daemonBash, probeDaemonHealth, @@ -84,6 +90,26 @@ const DEFAULT_WORKDIR = "/app"; // 32 bytes matches Docker's generation so audit logs don't vary by runner. const DAEMON_TOKEN_BYTES = 32; +/** + * Env keys mesh owns and a caller's `opts.env` MUST NOT shadow. DAEMON_TOKEN + * is the secrecy boundary; the rest configure the daemon's bootstrap and + * silently overriding any of them would break clone/install/dev-server start. + */ +const RESERVED_ENV_KEYS = new Set([ + "DAEMON_TOKEN", + "DAEMON_BOOT_ID", + "APP_ROOT", + "PROXY_PORT", + "DEV_PORT", + "RUNTIME", + "CLONE_URL", + "REPO_NAME", + "BRANCH", + "GIT_USER_NAME", + "GIT_USER_EMAIL", + "PACKAGE_MANAGER", +]); + // Default idle-reap TTL: 15 min from each ensure() hit. Every code-initiated // request flows through ensure() (or touches a record via getRecord, which // bumps the TTL on the K8s side), so an active sandbox pushes this forward; @@ -151,6 +177,11 @@ interface PortForwarder { localPort: number; } +interface RunnerTenant { + orgId: string; + userId: string; +} + interface K8sRecord { id: SandboxId; handle: string; @@ -167,6 +198,13 @@ interface K8sRecord { * itself, this is purely informational on the mesh side). */ daemonBootId: string; + /** + * Tenant identity carried through for metric attribution on subsequent + * operations (proxy, exec, delete) where the caller only has a handle. + * Null when ensure() was called without tenant context (smoke tests, + * adopt fallback when claim labels were absent). + */ + tenant: RunnerTenant | null; } interface PersistedK8sState { @@ -175,6 +213,7 @@ interface PersistedK8sState { workdir: string; workload?: Workload | null; daemonBootId?: string; + tenant?: RunnerTenant | null; [k: string]: unknown; } @@ -198,6 +237,13 @@ export interface KubernetesRunnerOptions { * deletes claim + pod when the wall clock passes that. */ idleTtlMs?: number; + /** + * OpenTelemetry meter for runner-level metrics (active gauge, ensure + * outcome counter, proxy duration histogram). Optional — when absent, + * runner is fully functional but emits no metrics. Tests typically pass + * undefined; mesh wires `metrics.getMeter("mesh", "1.0.0")`. + */ + meter?: Meter; } export class KubernetesSandboxRunner implements SandboxRunner { @@ -213,6 +259,12 @@ export class KubernetesSandboxRunner implements SandboxRunner { private readonly sandboxTemplateName: string; private readonly tokenGenerator: () => string; private readonly idleTtlMs: number; + /** + * Instruments are null when no meter was provided. All emit-paths must + * null-check; the alternative — passing the OTel API's no-op meter — would + * still allocate and dispatch on every call. + */ + private readonly metrics: RunnerMetrics | null; constructor(opts: KubernetesRunnerOptions = {}) { this.stateStore = opts.stateStore ?? null; @@ -226,6 +278,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { opts.tokenGenerator ?? (() => randomBytes(DAEMON_TOKEN_BYTES).toString("hex")); this.idleTtlMs = opts.idleTtlMs ?? DEFAULT_IDLE_TTL_MS; + this.metrics = opts.meter ? buildRunnerMetrics(opts.meter) : null; } // ---- SandboxRunner surface ------------------------------------------------ @@ -247,7 +300,13 @@ export class KubernetesSandboxRunner implements SandboxRunner { async delete(handle: string): Promise { const rec = await this.getRecord(handle); this.records.delete(handle); - if (rec) this.closeForwarder(rec.daemonForward); + if (rec) { + this.closeForwarder(rec.daemonForward); + // Decrement only when we actually held the record — getRecord can be + // null after restart-without-state-store, in which case the gauge + // was never incremented for this handle in this process. + this.metrics?.active.add(-1, tenantAttrs(rec.tenant)); + } await deleteSandboxClaim(this.kubeConfig, this.namespace, handle); if (this.stateStore) { if (rec) await this.stateStore.delete(rec.id, RUNNER_KIND); @@ -282,7 +341,25 @@ export class KubernetesSandboxRunner implements SandboxRunner { headers: { "content-type": "application/json" }, }); } - return proxyDaemonRequest(rec.daemonUrl, rec.token, path, init); + const start = performance.now(); + let status = 0; + try { + const resp = await proxyDaemonRequest( + rec.daemonUrl, + rec.token, + path, + init, + ); + status = resp.status; + return resp; + } finally { + this.recordProxyDuration( + "daemon", + status, + rec, + performance.now() - start, + ); + } } /** @@ -337,54 +414,80 @@ export class KubernetesSandboxRunner implements SandboxRunner { handle: string, request: Request, ): Promise { - const upstreamBase = await this.resolvePreviewUpstreamUrl(handle); - if (!upstreamBase) { - return jsonResponse(404, { error: "sandbox not found" }); - } + const start = performance.now(); + // In-memory cache only — preview is the hot path; a state-store hit per + // request would dominate latency. Tenant attribution is best-effort: when + // the records map is cold (mesh just restarted) the metric still records + // duration with empty tenant attrs. cAdvisor on the pod side covers + // bandwidth attribution authoritatively via pod labels. + const cachedRec = this.records.get(handle) ?? null; + let status = 0; + try { + const upstreamBase = await this.resolvePreviewUpstreamUrl(handle); + if (!upstreamBase) { + status = 404; + return jsonResponse(404, { error: "sandbox not found" }); + } - const reqUrl = new URL(request.url); - const isAdminPath = - reqUrl.pathname === "/_decopilot_vm" || - reqUrl.pathname.startsWith("/_decopilot_vm/"); - if (isAdminPath && request.method !== "GET") { - return jsonResponse(404, { error: "not found" }); - } + const reqUrl = new URL(request.url); + const isAdminPath = + reqUrl.pathname === "/_decopilot_vm" || + reqUrl.pathname.startsWith("/_decopilot_vm/"); + if (isAdminPath && request.method !== "GET") { + status = 404; + return jsonResponse(404, { error: "not found" }); + } - const target = `${upstreamBase}${reqUrl.pathname}${reqUrl.search}`; - const headers = new Headers(request.headers); - for (const h of PREVIEW_STRIP_REQUEST_HEADERS) headers.delete(h); - - const hasBody = request.method !== "GET" && request.method !== "HEAD"; - const init: RequestInit & { duplex?: string } = { - method: request.method, - headers, - body: hasBody ? request.body : undefined, - redirect: "manual", - signal: request.signal, - duplex: hasBody ? "half" : undefined, - }; + const target = `${upstreamBase}${reqUrl.pathname}${reqUrl.search}`; + const headers = new Headers(request.headers); + for (const h of PREVIEW_STRIP_REQUEST_HEADERS) headers.delete(h); + + const hasBody = request.method !== "GET" && request.method !== "HEAD"; + const init: RequestInit & { duplex?: string } = { + method: request.method, + headers, + body: hasBody ? request.body : undefined, + redirect: "manual", + signal: request.signal, + duplex: hasBody ? "half" : undefined, + }; - let upstream: Response; - try { - upstream = await fetch(target, init as RequestInit); - } catch (err) { - console.warn( - `[${LOG_LABEL}] preview fetch to ${target} failed: ${err instanceof Error ? err.message : String(err)}`, - ); - return jsonResponse(502, { error: "sandbox daemon unreachable" }); - } + let upstream: Response; + try { + upstream = await fetch(target, init as RequestInit); + } catch (err) { + // Truncate to host+pathname — query strings can carry secrets + // (magic-link tokens, signed URLs) and would otherwise end up in + // mesh stdout → kubectl logs → log aggregator. + const safeTarget = `${upstreamBase}${reqUrl.pathname}`; + console.warn( + `[${LOG_LABEL}] preview fetch to ${safeTarget} failed: ${err instanceof Error ? err.message : String(err)}`, + ); + status = 502; + return jsonResponse(502, { error: "sandbox daemon unreachable" }); + } - const responseHeaders = new Headers(); - for (const [k, v] of upstream.headers.entries()) { - if (!PREVIEW_STRIP_RESPONSE_HEADERS.includes(k.toLowerCase())) { - responseHeaders.set(k, v); + const responseHeaders = new Headers(); + for (const [k, v] of upstream.headers.entries()) { + if (!PREVIEW_STRIP_RESPONSE_HEADERS.includes(k.toLowerCase())) { + responseHeaders.set(k, v); + } } + status = upstream.status; + return new Response(upstream.body, { + status: upstream.status, + statusText: upstream.statusText, + headers: responseHeaders, + }); + } finally { + this.recordProxyDuration( + "preview", + status, + cachedRec, + performance.now() - start, + handle, + ); } - return new Response(upstream.body, { - status: upstream.status, - statusText: upstream.statusText, - headers: responseHeaders, - }); } // ---- Ensure flow ---------------------------------------------------------- @@ -400,11 +503,6 @@ export class KubernetesSandboxRunner implements SandboxRunner { `[${LOG_LABEL}] opts.image ignored (template ${this.sandboxTemplateName} pins image): got ${opts.image}`, ); } - if (opts.env && Object.keys(opts.env).length > 0) { - console.warn( - `[${LOG_LABEL}] opts.env ignored (template holds non-token env; DAEMON_TOKEN is injected per-claim): keys=${Object.keys(opts.env).join(",")}`, - ); - } // 1. State-store resume. if (ops) { @@ -417,6 +515,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { ops, /* persistNow */ false, /* patchTtl */ true, + "resume", ); await ops.delete(id, RUNNER_KIND); } @@ -441,6 +540,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { ops, /* persistNow */ true, /* patchTtl */ true, + "adopt", ); await deleteSandboxClaim(this.kubeConfig, this.namespace, handle).catch( () => {}, @@ -448,7 +548,13 @@ export class KubernetesSandboxRunner implements SandboxRunner { } // 3. Fresh provision. const fresh = await this.provision(id, handle, opts); - return this.finish(fresh, ops, /* persistNow */ true, /* patchTtl */ false); + return this.finish( + fresh, + ops, + /* persistNow */ true, + /* patchTtl */ false, + "fresh", + ); } private async finish( @@ -456,7 +562,9 @@ export class KubernetesSandboxRunner implements SandboxRunner { ops: RunnerStateStoreOps | null, persistNow: boolean, patchTtl: boolean, + outcome: "fresh" | "resume" | "adopt", ): Promise { + const wasCached = this.records.has(rec.handle); this.records.set(rec.handle, rec); if (persistNow) await this.persist(ops, rec); // Fresh provision set a shutdownTime in the claim spec already; resumes @@ -473,35 +581,57 @@ export class KubernetesSandboxRunner implements SandboxRunner { ), ); } + if (this.metrics) { + const attrs = tenantAttrs(rec.tenant); + this.metrics.ensureOutcome.add(1, { ...attrs, outcome }); + // Only increment the active gauge on first observation to avoid + // double-counting when the same handle is rehydrated multiple times + // (mesh-process internal cache hit; ensureLocked is invoked again). + if (!wasCached) this.metrics.active.add(1, attrs); + } return this.toSandbox(rec); } - private async provision( - id: SandboxId, - handle: string, + /** + * Compose the env block the daemon's orchestrator reads to clone, install, + * and start the dev server. Mirrors the docker runner's contract; reader is + * `packages/sandbox/daemon/config.ts`. + * + * Caller-supplied `opts.env` is layered first so the bootstrap keys defined + * here (and listed in RESERVED_ENV_KEYS) always win — an intercepted + * DAEMON_TOKEN would compromise the sandbox; an intercepted DEV_PORT would + * just break the boot. We warn — not throw — to match the docker runner's + * permissive shape. + */ + private buildEnvMap( opts: EnsureOptions, - ): Promise { - const token = this.tokenGenerator(); - const daemonBootId = randomUUID(); - const workdir = DEFAULT_WORKDIR; - const devContainerPort = opts.workload?.devPort ?? DEFAULT_DEV_PORT; - const runtime = opts.workload?.runtime ?? "node"; - const packageManager = opts.workload?.packageManager ?? null; - const repo = opts.repo ?? null; + boot: { token: string; daemonBootId: string; workdir: string }, + ): Record { + const callerEnv: Record = {}; + const dropped: string[] = []; + for (const [k, v] of Object.entries(opts.env ?? {})) { + if (RESERVED_ENV_KEYS.has(k)) dropped.push(k); + else callerEnv[k] = v; + } + if (dropped.length > 0) { + console.warn( + `[${LOG_LABEL}] opts.env keys overlap reserved bootstrap names and were dropped: ${dropped.join(",")}`, + ); + } + + const repo = opts.repo; const repoLabel = repo ? (repo.displayName ?? deriveRepoLabel(repo.cloneUrl)) : null; - // Full env contract — daemon's orchestrator owns clone + install + - // dev-server start; no external bootstrap call needed. Mirrors the - // docker runner's env contract; reader is `packages/sandbox/daemon/config.ts`. - const envMap: Record = { - DAEMON_TOKEN: token, - DAEMON_BOOT_ID: daemonBootId, - APP_ROOT: workdir, + return { + ...callerEnv, + DAEMON_TOKEN: boot.token, + DAEMON_BOOT_ID: boot.daemonBootId, + APP_ROOT: boot.workdir, PROXY_PORT: String(DAEMON_CONTAINER_PORT), - DEV_PORT: String(devContainerPort), - RUNTIME: runtime, + DEV_PORT: String(opts.workload?.devPort ?? DEFAULT_DEV_PORT), + RUNTIME: opts.workload?.runtime ?? "node", ...(repo ? { CLONE_URL: repo.cloneUrl, @@ -511,18 +641,31 @@ export class KubernetesSandboxRunner implements SandboxRunner { GIT_USER_EMAIL: repo.userEmail, } : {}), - ...(packageManager ? { PACKAGE_MANAGER: packageManager } : {}), + ...(opts.workload?.packageManager + ? { PACKAGE_MANAGER: opts.workload.packageManager } + : {}), }; + } - const claim: SandboxClaim = { + private buildClaim( + handle: string, + opts: EnsureOptions, + boot: { token: string; daemonBootId: string; workdir: string }, + ): SandboxClaim { + const envMap = this.buildEnvMap(opts, boot); + return { apiVersion: `${K8S_CONSTANTS.CLAIM_API_GROUP}/${K8S_CONSTANTS.CLAIM_API_VERSION}`, kind: "SandboxClaim", metadata: { name: handle, namespace: this.namespace, + // Tenant duplicated on the claim itself (not just the pod) so the + // adopt path can recover orgId/userId after a state-store wipe; + // adopt() reads claim.metadata.labels, not pod labels. labels: { "app.kubernetes.io/name": "mesh-sandbox", "app.kubernetes.io/managed-by": "mesh", + ...buildTenantLabels(opts.tenant), }, }, spec: { @@ -533,12 +676,19 @@ export class KubernetesSandboxRunner implements SandboxRunner { // distinguishes claimed pods from warm-pool pods (template sets // role=sandbox-pod by default). additionalPodMetadata: { - labels: buildTenantPodLabels(handle, opts.tenant), + labels: buildTenantLabels(opts.tenant, { + [LABEL_KEYS.role]: "claimed", + [LABEL_KEYS.sandboxHandle]: handle, + }), }, // `valueFrom.secretKeyRef` isn't supported on SandboxClaim env; RBAC // on the namespace is the secrecy boundary. Warm-pool off because the - // operator rejects custom env on warm-pooled claims. - env: Object.entries(envMap).map(([name, value]) => ({ name, value })), + // operator rejects custom env on warm-pooled claims. Sorted by name + // so `kubectl diff` / claim audit entries don't churn across runs + // that pass the same env in different insertion orders. + env: Object.entries(envMap) + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map(([name, value]) => ({ name, value })), warmpool: "none", lifecycle: { shutdownPolicy: "Delete", @@ -546,7 +696,22 @@ export class KubernetesSandboxRunner implements SandboxRunner { }, }, }; + } + + private async provision( + id: SandboxId, + handle: string, + opts: EnsureOptions, + ): Promise { + const token = this.tokenGenerator(); + const daemonBootId = randomUUID(); + const workdir = DEFAULT_WORKDIR; + const claim = this.buildClaim(handle, opts, { + token, + daemonBootId, + workdir, + }); await createSandboxClaim(this.kubeConfig, this.namespace, claim); const { podName } = await waitForSandboxReady( this.kubeConfig, @@ -580,6 +745,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { daemonForward, workload: opts.workload ?? null, daemonBootId, + tenant: opts.tenant ?? null, }; } @@ -608,27 +774,14 @@ export class KubernetesSandboxRunner implements SandboxRunner { // annotation over the persisted value. const currentPodName = readPodName(claim) ?? state.podName; - const daemonForward = await this.openForwarder( - currentPodName, - DAEMON_CONTAINER_PORT, - handle, - ).catch(() => null); - if (!daemonForward) return null; - const daemonUrl = `http://127.0.0.1:${daemonForward.localPort}`; - - // probeDaemonHealth returns null when /health is unreachable OR lacks a - // bootId (older daemon shape). Either way, purge + re-provision. - const health = await probeDaemonHealth(daemonUrl); - if (!health) { - this.closeForwarder(daemonForward); - return null; - } + const live = await this.openAndProbeDaemon(currentPodName, handle); + if (!live) return null; // Pod bounced but the daemon's orchestrator handles re-bootstrap itself // on boot (resume-on-restart). Just refresh our copy of bootId. - if (state.daemonBootId && state.daemonBootId !== health.bootId) { + if (state.daemonBootId && state.daemonBootId !== live.bootId) { console.warn( - `[${LOG_LABEL}] daemon restart detected (handle=${handle}): stored bootId=${state.daemonBootId} live bootId=${health.bootId}`, + `[${LOG_LABEL}] daemon restart detected (handle=${handle}): stored bootId=${state.daemonBootId} live bootId=${live.bootId}`, ); } @@ -638,10 +791,11 @@ export class KubernetesSandboxRunner implements SandboxRunner { podName: currentPodName, token: state.token, workdir: state.workdir ?? DEFAULT_WORKDIR, - daemonUrl, - daemonForward, + daemonUrl: live.daemonUrl, + daemonForward: live.daemonForward, workload: state.workload ?? null, - daemonBootId: health.bootId, + daemonBootId: live.bootId, + tenant: state.tenant ?? null, }; } @@ -656,29 +810,55 @@ export class KubernetesSandboxRunner implements SandboxRunner { const token = readClaimDaemonToken(claim); if (!token) return null; + const live = await this.openAndProbeDaemon(podName, handle); + if (!live) return null; + + return { + id, + handle, + podName, + token, + workdir: DEFAULT_WORKDIR, + daemonUrl: live.daemonUrl, + daemonForward: live.daemonForward, + workload: null, + daemonBootId: live.bootId, + // Recovered from claim labels written at provision time. Null if the + // claim pre-dates tenant labelling (back-compat with already-running + // sandboxes when this code rolls out). + tenant: readClaimTenant(claim), + }; + } + + /** + * Open the daemon port-forward and probe `/health`. Closes the forwarder + * and returns null on any failure so the caller can fall through to + * recreate. Both `rehydrate` and `adopt` share this shape — the only + * difference is whether the bootId match is checked. + */ + private async openAndProbeDaemon( + podName: string, + handle: string, + ): Promise<{ + daemonForward: PortForwarder; + daemonUrl: string; + bootId: string; + } | null> { const daemonForward = await this.openForwarder( podName, DAEMON_CONTAINER_PORT, handle, - ); + ).catch(() => null); + if (!daemonForward) return null; const daemonUrl = `http://127.0.0.1:${daemonForward.localPort}`; + // probeDaemonHealth returns null when /health is unreachable OR lacks a + // bootId (older daemon shape). Either way, purge + re-provision. const health = await probeDaemonHealth(daemonUrl); if (!health) { this.closeForwarder(daemonForward); return null; } - - return { - id, - handle, - podName, - token, - workdir: DEFAULT_WORKDIR, - daemonUrl, - daemonForward, - workload: null, - daemonBootId: health.bootId, - }; + return { daemonForward, daemonUrl, bootId: health.bootId }; } // ---- Handle resolution (post-restart) ------------------------------------- @@ -700,6 +880,24 @@ export class KubernetesSandboxRunner implements SandboxRunner { return rec; } + // ---- Metric helpers ------------------------------------------------------- + + private recordProxyDuration( + source: "daemon" | "preview", + statusCode: number, + rec: K8sRecord | null, + durationMs: number, + fallbackHandle?: string, + ): void { + if (!this.metrics) return; + this.metrics.proxyDurationMs.record(durationMs, { + ...tenantAttrs(rec?.tenant ?? null), + source, + sandbox_handle: rec?.handle ?? fallbackHandle ?? "", + status_code: statusCode || 0, + }); + } + // ---- Identity + preview URL ---------------------------------------------- private computeHandle(id: SandboxId): string { @@ -741,6 +939,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { workdir: rec.workdir, workload: rec.workload, daemonBootId: rec.daemonBootId, + tenant: rec.tenant, }; await ops.put(rec.id, RUNNER_KIND, { handle: rec.handle, state }); } @@ -775,6 +974,12 @@ export class KubernetesSandboxRunner implements SandboxRunner { ); server.once("error", (err: NodeJS.ErrnoException) => { if (err.code === "EADDRINUSE" && attempt < PORT_WALK_LIMIT) { + // Release the failed listener before walking forward — listen() + // failure leaves the Server object holding the connection handler + // closure; closing makes the leak trivially visible to GC. + try { + server.close(); + } catch {} const next = PORT_RANGE_START + ((port - PORT_RANGE_START + 1) % PORT_RANGE_SIZE); @@ -873,6 +1078,32 @@ export class KubernetesSandboxRunner implements SandboxRunner { // ---- Helpers ---------------------------------------------------------------- +interface RunnerMetrics { + active: UpDownCounter; + ensureOutcome: Counter; + proxyDurationMs: Histogram; +} + +function buildRunnerMetrics(meter: Meter): RunnerMetrics { + return { + active: meter.createUpDownCounter("mesh.sandbox.active", { + description: + "Active sandbox count, by runner kind and owning org. Cross-checks the cAdvisor-derived count from the cluster — divergence between the two indicates orphaned claims (mesh deleted but K8s didn't reap) or unattributed pods.", + unit: "{sandbox}", + }), + ensureOutcome: meter.createCounter("mesh.sandbox.ensure.outcome", { + description: + "Outcome of each ensure() call: fresh provision, resume from state-store after restart, or adopt of a cluster-side claim mesh didn't know about. Cold-start ratio is the primary input for warm-pool sizing.", + unit: "{call}", + }), + proxyDurationMs: meter.createHistogram("mesh.sandbox.proxy.duration_ms", { + description: + "Wall-clock latency of mesh-mediated requests to the sandbox daemon: tool exec proxies (source=daemon) and preview iframe traffic (source=preview).", + unit: "ms", + }), + }; +} + function loadDefaultKubeConfig(): KubeConfig { const kc = new KubeConfigClass(); kc.loadFromDefault(); @@ -928,6 +1159,15 @@ function jsonResponse(status: number, body: unknown): Response { }); } +// K8s label keys mesh attaches. Centralized so writers (buildTenantLabels) +// and the reader (readClaimTenant) can't drift. +const LABEL_KEYS = { + role: "mesh.decocms.com/role", + sandboxHandle: "mesh.decocms.com/sandbox-handle", + orgId: "mesh.decocms.com/org-id", + userId: "mesh.decocms.com/user-id", +} as const; + // K8s label values: ≤63 chars, must match `(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?`. // Org/user IDs are UUIDs in mesh and pass through unchanged; the regex check // + truncation is defensive against future ID-shape changes (the operator will @@ -941,28 +1181,53 @@ function sanitizeLabelValue(value: string): string { } /** - * Pod labels for cost attribution. `role=claimed` distinguishes these from - * warm-pool pods (template sets `role=sandbox-pod` as the default). Sandbox - * handle is included so dashboards can drill from per-org aggregates down to - * a specific sandbox without needing a join table. + * Tenant labels for `adopt()` recovery + cost attribution. Used on both the + * claim (so `kubectl get sandboxclaim` shows ownership and adopt() can read + * orgId/userId after a state-store wipe) and the pod (where cAdvisor / + * kubelet metrics pick them up). Pass `extra` for pod-only fields like + * `role` and `sandbox-handle`. */ -function buildTenantPodLabels( - handle: string, +function buildTenantLabels( tenant: EnsureOptions["tenant"], + extra: Record = {}, ): Record { - const labels: Record = { - "mesh.decocms.com/role": "claimed", - "mesh.decocms.com/sandbox-handle": handle, - }; + const labels: Record = { ...extra }; if (tenant) { const orgId = sanitizeLabelValue(tenant.orgId); const userId = sanitizeLabelValue(tenant.userId); - if (orgId) labels["mesh.decocms.com/org-id"] = orgId; - if (userId) labels["mesh.decocms.com/user-id"] = userId; + if (orgId) labels[LABEL_KEYS.orgId] = orgId; + if (userId) labels[LABEL_KEYS.userId] = userId; } return labels; } +/** Read tenant back from a claim's metadata.labels (adopt path). */ +function readClaimTenant(claim: SandboxResource): RunnerTenant | null { + const labels = claim.metadata?.labels; + if (!labels) return null; + const orgId = labels[LABEL_KEYS.orgId]; + const userId = labels[LABEL_KEYS.userId]; + if (!orgId || !userId) return null; + return { orgId, userId }; +} + +/** + * Convert tenant struct to OTel attribute keys. `runner_kind` is constant for + * a given runner instance but included on every attrs set so downstream + * dashboards can pivot across runners (k8s vs docker) without re-aggregating. + */ +function tenantAttrs(tenant: RunnerTenant | null): { + org_id: string; + user_id: string; + runner_kind: string; +} { + return { + org_id: tenant?.orgId ?? "", + user_id: tenant?.userId ?? "", + runner_kind: RUNNER_KIND, + }; +} + /** Fallback for when callers don't provide `repo.displayName`. */ function deriveRepoLabel(cloneUrl: string): string { try { From 8235065634e97f29383adc68097667d6f8a91af8 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 14:02:54 -0300 Subject: [PATCH 13/16] Update Helm chart values to set default nodeSelector for amd64 architecture - Configured `nodeSelector` in `values.yaml` to specify `kubernetes.io/arch: amd64`, ensuring compatibility with amd64 node groups. - Added comments to guide users on overriding this setting for arm64 clusters, enhancing clarity for deployment configurations. These changes improve the deployment flexibility for different architecture environments. --- deploy/helm/values.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index e55594a5c7..fc15c33c5b 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -129,12 +129,12 @@ terminationGracePeriodSeconds: 60 # Optional lifecycle hooks (preStop, postStart) # lifecycle: {} -nodeSelector: {} -# Pin arch in your prod values override if your node groups are heterogeneous. -# Default is unset so the chart works on arm64 (Apple Silicon kind, ARM EKS, -# bare-metal RPi clusters). Example for AWS amd64 node groups: -# nodeSelector: -# kubernetes.io/arch: amd64 +nodeSelector: + kubernetes.io/arch: amd64 +# arm64 clusters (Apple Silicon kind, ARM EKS, bare-metal RPi) should override +# this — set `nodeSelector: {}` or pin to `arm64`. Mesh's own image is +# multi-arch, but the s3-sync sidecar and some upstream subchart images may +# be amd64-only. tolerations: [] # Example: From e38bee67e38d4bd45cf3b10fdfeac31d495f8f15 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 16:32:06 -0300 Subject: [PATCH 14/16] Remove scheduled_tasks.lock file as it is no longer needed for task management. This cleanup helps streamline the project structure. --- .claude/scheduled_tasks.lock | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .claude/scheduled_tasks.lock diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index 40db5a12f4..0000000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"81d7daa7-d78f-44e2-8618-ad6a048542d9","pid":31553,"procStart":"Tue Apr 28 04:50:17 2026","acquiredAt":1777356837679} \ No newline at end of file From e89dda9b4025b6a219e51d344cd1ea9eb4c16664 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 17:28:05 -0300 Subject: [PATCH 15/16] Add release workflow for Studio Sandbox image and update references - Introduced a GitHub Actions workflow to build and push the Studio Sandbox Docker image upon changes to the `packages/sandbox` directory. - Updated environment variable references from `MESH_SANDBOX_PREVIEW_URL_PATTERN` to `STUDIO_SANDBOX_PREVIEW_URL_PATTERN` across multiple files to reflect the new naming convention. - Adjusted related test cases and configurations to ensure consistency with the new sandbox naming scheme. These changes enhance the deployment process and improve clarity in the codebase regarding the Studio Sandbox environment. --- ...ndbox.yaml => release-studio-sandbox.yaml} | 2 +- apps/mesh/src/index.ts | 4 +- apps/mesh/src/sandbox/lifecycle.ts | 2 +- apps/mesh/src/sandbox/preview-proxy.test.ts | 40 ++-- apps/mesh/src/sandbox/preview-proxy.ts | 4 +- deploy/helm/Chart.yaml | 1 + deploy/helm/README.md | 2 +- deploy/helm/examples/values-kind.yaml | 2 +- deploy/helm/templates/_helpers.tpl | 19 ++ deploy/helm/templates/configmap.yaml | 2 +- .../templates/sandbox-network-policy.yaml | 8 +- .../helm/templates/sandbox-preview-cert.yaml | 2 +- .../templates/sandbox-preview-gateway.yaml | 4 +- deploy/helm/templates/sandbox-rbac.yaml | 6 +- deploy/helm/templates/sandbox-template.yaml | 12 +- deploy/helm/templates/sandbox-warm-pool.yaml | 6 +- deploy/helm/templates/validations.yaml | 1 + deploy/helm/values.yaml | 16 +- deploy/k8s-sandbox/local/README.md | 18 +- deploy/k8s-sandbox/local/down.sh | 2 +- .../values-kube-prometheus-stack.local.yaml | 4 +- deploy/k8s-sandbox/local/reload-image.sh | 4 +- .../k8s-sandbox/local/sandbox-template.yaml | 10 +- deploy/k8s-sandbox/local/smoke.ts | 6 +- deploy/k8s-sandbox/local/up.sh | 10 +- deploy/k8s-sandbox/monitoring/README.md | 8 +- .../dashboards/sandbox-overview.json | 14 +- .../monitoring/values-otel-collector.yaml | 10 +- .../sandbox/server/runner/k8s/client.test.ts | 10 +- packages/sandbox/server/runner/k8s/runner.ts | 179 ++++++++++++++++-- 30 files changed, 296 insertions(+), 112 deletions(-) rename .github/workflows/{release-mesh-sandbox.yaml => release-studio-sandbox.yaml} (98%) diff --git a/.github/workflows/release-mesh-sandbox.yaml b/.github/workflows/release-studio-sandbox.yaml similarity index 98% rename from .github/workflows/release-mesh-sandbox.yaml rename to .github/workflows/release-studio-sandbox.yaml index 750f0e58be..dd83791b63 100644 --- a/.github/workflows/release-mesh-sandbox.yaml +++ b/.github/workflows/release-studio-sandbox.yaml @@ -1,4 +1,4 @@ -name: Release Mesh Sandbox Image +name: Release Studio Sandbox Image on: push: diff --git a/apps/mesh/src/index.ts b/apps/mesh/src/index.ts index 014b6e9519..99b7f520d5 100644 --- a/apps/mesh/src/index.ts +++ b/apps/mesh/src/index.ts @@ -79,7 +79,7 @@ function withSecurityHeaders(res: Response): Response { let ingressServers: import("node:net").Server[] = []; // Sandbox preview reverse-proxy (K8s only). The base domain is parsed at -// boot from MESH_SANDBOX_PREVIEW_URL_PATTERN; null disables the proxy and +// boot from STUDIO_SANDBOX_PREVIEW_URL_PATTERN; null disables the proxy and // preview-host requests fall through to the normal mesh routing (which 404s // because nothing matches). The Bun-level WS handler is registered // unconditionally — when previewBaseDomain is null, no upgrade path runs it. @@ -94,7 +94,7 @@ const { getOrInitSharedRunner: getOrInitRunnerForPreview } = await import( "./sandbox/lifecycle" ); const previewBaseDomain = parsePreviewBaseDomain( - process.env.MESH_SANDBOX_PREVIEW_URL_PATTERN, + process.env.STUDIO_SANDBOX_PREVIEW_URL_PATTERN, ); const previewProxyDeps = { baseDomain: previewBaseDomain ?? "", diff --git a/apps/mesh/src/sandbox/lifecycle.ts b/apps/mesh/src/sandbox/lifecycle.ts index 660c19b2af..9cb1fd3d72 100644 --- a/apps/mesh/src/sandbox/lifecycle.ts +++ b/apps/mesh/src/sandbox/lifecycle.ts @@ -52,7 +52,7 @@ function resolveOnce( // 127.0.0.1 port-forward path and emits a URL the user's browser can // actually reach. Empty/unset = local forwarder fallback (dev). function readPreviewUrlPattern(): string | undefined { - const raw = process.env.MESH_SANDBOX_PREVIEW_URL_PATTERN; + const raw = process.env.STUDIO_SANDBOX_PREVIEW_URL_PATTERN; return raw && raw.trim() !== "" ? raw : undefined; } diff --git a/apps/mesh/src/sandbox/preview-proxy.test.ts b/apps/mesh/src/sandbox/preview-proxy.test.ts index 59ff1cb2ae..3803c6c71f 100644 --- a/apps/mesh/src/sandbox/preview-proxy.test.ts +++ b/apps/mesh/src/sandbox/preview-proxy.test.ts @@ -61,22 +61,22 @@ describe("parsePreviewBaseDomain", () => { describe("extractHandleFromHost", () => { const base = "preview.decocms.com"; - it("extracts mesh-sb- handles from the matching subdomain", () => { + it("extracts studio-sb- handles from the matching subdomain", () => { expect( - extractHandleFromHost("mesh-sb-abc123.preview.decocms.com", base), - ).toBe("mesh-sb-abc123"); + extractHandleFromHost("studio-sb-abc123.preview.decocms.com", base), + ).toBe("studio-sb-abc123"); }); it("ignores port suffix in Host header", () => { expect( - extractHandleFromHost("mesh-sb-abc.preview.decocms.com:8080", base), - ).toBe("mesh-sb-abc"); + extractHandleFromHost("studio-sb-abc.preview.decocms.com:8080", base), + ).toBe("studio-sb-abc"); }); it("is case-insensitive on host + base", () => { - expect(extractHandleFromHost("Mesh-Sb-ABC.Preview.DecocMs.com", base)).toBe( - "mesh-sb-abc", - ); + expect( + extractHandleFromHost("Studio-Sb-ABC.Preview.DecocMs.com", base), + ).toBe("studio-sb-abc"); }); it("returns null when the handle prefix is missing", () => { @@ -87,15 +87,15 @@ describe("extractHandleFromHost", () => { it("returns null when the base domain doesn't match", () => { expect( - extractHandleFromHost("mesh-sb-abc.preview.example.org", base), + extractHandleFromHost("studio-sb-abc.preview.example.org", base), ).toBeNull(); }); it("rejects nested subdomains", () => { - // foo.mesh-sb-abc.preview.decocms.com → strip suffix yields - // "foo.mesh-sb-abc" which has a dot → null. + // foo.studio-sb-abc.preview.decocms.com → strip suffix yields + // "foo.studio-sb-abc" which has a dot → null. expect( - extractHandleFromHost("foo.mesh-sb-abc.preview.decocms.com", base), + extractHandleFromHost("foo.studio-sb-abc.preview.decocms.com", base), ).toBeNull(); }); @@ -103,7 +103,7 @@ describe("extractHandleFromHost", () => { expect(extractHandleFromHost(null, base)).toBeNull(); expect(extractHandleFromHost(undefined, base)).toBeNull(); expect( - extractHandleFromHost("mesh-sb-abc.preview.decocms.com", ""), + extractHandleFromHost("studio-sb-abc.preview.decocms.com", ""), ).toBeNull(); }); }); @@ -114,7 +114,7 @@ describe("applyPreviewPattern <-> parse/extract round-trip", () => { // inverses. If either side ever supports a pattern shape the other doesn't // recognize, this test catches the mismatch before it silently misroutes // production traffic. - const handle = "mesh-sb-abc123"; + const handle = "studio-sb-abc123"; const patterns = [ "https://{handle}.preview.decocms.com", @@ -150,8 +150,8 @@ describe("tryHandlePreviewHttp", () => { }); it("returns 503 when the runner isn't configured for K8s", async () => { - const req = new Request("https://mesh-sb-abc.preview.example.com/", { - headers: { host: "mesh-sb-abc.preview.example.com" }, + const req = new Request("https://studio-sb-abc.preview.example.com/", { + headers: { host: "studio-sb-abc.preview.example.com" }, }); const res = await tryHandlePreviewHttp(req, { baseDomain, @@ -170,9 +170,9 @@ describe("tryHandlePreviewHttp", () => { }, }; const req = new Request( - "https://mesh-sb-deadbeef.preview.example.com/foo", + "https://studio-sb-deadbeef.preview.example.com/foo", { - headers: { host: "mesh-sb-deadbeef.preview.example.com" }, + headers: { host: "studio-sb-deadbeef.preview.example.com" }, }, ); const res = await tryHandlePreviewHttp(req, { @@ -183,13 +183,13 @@ describe("tryHandlePreviewHttp", () => { expect(res).not.toBeNull(); expect(res!.status).toBe(200); expect(received).not.toBeNull(); - expect(received!.handle).toBe("mesh-sb-deadbeef"); + expect(received!.handle).toBe("studio-sb-deadbeef"); }); }); describe("tryUpgradePreviewWs", () => { const baseDomain = "preview.example.com"; - const previewHost = "mesh-sb-abc123.preview.example.com"; + const previewHost = "studio-sb-abc123.preview.example.com"; function wsRequest(path: string, host: string = previewHost): Request { return new Request(`https://${host}${path}`, { diff --git a/apps/mesh/src/sandbox/preview-proxy.ts b/apps/mesh/src/sandbox/preview-proxy.ts index 86a153cbf2..74d7a81465 100644 --- a/apps/mesh/src/sandbox/preview-proxy.ts +++ b/apps/mesh/src/sandbox/preview-proxy.ts @@ -32,7 +32,7 @@ const MAX_PENDING_FRAMES = 256; /** * Parses the base preview hostname (e.g. `preview.decocms.com`) out of the - * `MESH_SANDBOX_PREVIEW_URL_PATTERN` value. The pattern has the form + * `STUDIO_SANDBOX_PREVIEW_URL_PATTERN` value. The pattern has the form * `https://{handle}.preview.example.com` (or `https://{handle}.`), * matching what the K8s runner's `applyPreviewPattern` produces. Returns * null when the pattern is empty/missing/malformed — preview proxying is @@ -71,7 +71,7 @@ export function parsePreviewBaseDomain( /** * Pulls the sandbox handle out of a request Host header. Returns null when * the host doesn't match `.` or the handle doesn't carry - * the K8s runner's `mesh-sb-` prefix (anything else means the request isn't + * the K8s runner's `studio-sb-` prefix (anything else means the request isn't * for a mesh sandbox preview and should fall through to the rest of the * mesh API). */ diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml index 39c9109008..67a0fa30b8 100644 --- a/deploy/helm/Chart.yaml +++ b/deploy/helm/Chart.yaml @@ -4,6 +4,7 @@ description: A Helm chart for deco Studio self-hosted deployment type: application version: 0.6.2 appVersion: "latest" +kubeVersion: ">=1.30.0-0" dependencies: - name: nats diff --git a/deploy/helm/README.md b/deploy/helm/README.md index 12fe48970a..1e48ee17b7 100644 --- a/deploy/helm/README.md +++ b/deploy/helm/README.md @@ -1364,7 +1364,7 @@ installs, in order: — operator Deployment + four `v1alpha1` CRDs (`Sandbox`, `SandboxClaim`, `SandboxTemplate`, `SandboxWarmPool`) in the `agent-sandbox-system` namespace. -- A `SandboxTemplate` named `mesh-sandbox` — the shared pod template every +- A `SandboxTemplate` named `studio-sandbox` — the shared pod template every `SandboxClaim` references. Image, pull policy, and resources come from `sandbox.kubernetes.*`. - A `NetworkPolicy` that scopes sandbox-pod ingress/egress (see diff --git a/deploy/helm/examples/values-kind.yaml b/deploy/helm/examples/values-kind.yaml index f659f794d5..c5f3e780a6 100644 --- a/deploy/helm/examples/values-kind.yaml +++ b/deploy/helm/examples/values-kind.yaml @@ -140,7 +140,7 @@ sandbox: # `*.localhost` is special — browsers resolve every subdomain of # localhost to 127.0.0.1 without /etc/hosts entries. Combined with the # user's `kubectl port-forward svc/deco-studio 8080:80`, this means - # browser → `mesh-sb-XXX.preview.localhost:8080` → kube port-forward → + # browser → `studio-sb-XXX.preview.localhost:8080` → kube port-forward → # mesh edge → preview-proxy → sandbox daemon Service. No ingress, no DNS, # no certs. Match the port to whatever the user port-forwards to. previewUrlPattern: "http://{handle}.preview.localhost:8080" diff --git a/deploy/helm/templates/_helpers.tpl b/deploy/helm/templates/_helpers.tpl index d3eb01d605..0f61f166f0 100644 --- a/deploy/helm/templates/_helpers.tpl +++ b/deploy/helm/templates/_helpers.tpl @@ -178,6 +178,25 @@ Validate OTel collector/S3 configuration. {{- end }} {{- end }} +{{/* +Validate that Gateway API + cert-manager CRDs are present when the sandbox +preview gateway is enabled. Without this check, `helm install` would push +Gateway/HTTPRoute/Certificate to an API server that doesn't know those +kinds — the failure mode is an opaque "no matches for kind" rejection, +sometimes after partial-apply. Failing at template time keeps the release +atomic and gives a pointer to the right install command. +*/}} +{{- define "chart-deco-studio.validateSandboxPreviewGateway" -}} +{{- if and .Values.sandbox.kubernetes.enabled .Values.sandbox.kubernetes.previewGateway.enabled }} +{{- if not (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1") }} +{{- fail "chart-deco-studio: sandbox.kubernetes.previewGateway.enabled=true requires the Gateway API CRDs (gateway.networking.k8s.io/v1). Install: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml — and a Gateway controller (Istio, Envoy Gateway, Cilium, ...) implementing the chosen gatewayClassName." -}} +{{- end }} +{{- if not (.Capabilities.APIVersions.Has "cert-manager.io/v1") }} +{{- fail "chart-deco-studio: sandbox.kubernetes.previewGateway.enabled=true requires cert-manager (cert-manager.io/v1). Install: helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set crds.enabled=true" -}} +{{- end }} +{{- end }} +{{- end }} + {{/* Formats OTEL headers map as key=value,key2=value2 format. */}} diff --git a/deploy/helm/templates/configmap.yaml b/deploy/helm/templates/configmap.yaml index e655507e17..499e074cd9 100644 --- a/deploy/helm/templates/configmap.yaml +++ b/deploy/helm/templates/configmap.yaml @@ -22,6 +22,6 @@ data: MESH_SANDBOX_RUNNER: "kubernetes" {{- end }} {{- with .Values.sandbox.kubernetes.previewUrlPattern }} - MESH_SANDBOX_PREVIEW_URL_PATTERN: {{ . | quote }} + STUDIO_SANDBOX_PREVIEW_URL_PATTERN: {{ . | quote }} {{- end }} {{- end }} diff --git a/deploy/helm/templates/sandbox-network-policy.yaml b/deploy/helm/templates/sandbox-network-policy.yaml index 5ab36fb680..864d4bcfb3 100644 --- a/deploy/helm/templates/sandbox-network-policy.yaml +++ b/deploy/helm/templates/sandbox-network-policy.yaml @@ -6,7 +6,7 @@ # shape. # # Scope: selects pods in agent-sandbox-system labeled -# app.kubernetes.io/name=mesh-sandbox. Applies both ingress and egress rules, +# app.kubernetes.io/name=studio-sandbox. Applies both ingress and egress rules, # so egress is deny-by-default (policyType Egress with only allowed rules). # # Threat model: workload is arbitrary user code. Egress must not reach IMDS @@ -16,16 +16,16 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: mesh-sandbox + name: studio-sandbox namespace: agent-sandbox-system labels: - app.kubernetes.io/name: mesh-sandbox + app.kubernetes.io/name: studio-sandbox app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "chart-deco-studio.chart" . }} spec: podSelector: matchLabels: - app.kubernetes.io/name: mesh-sandbox + app.kubernetes.io/name: studio-sandbox policyTypes: - Ingress - Egress diff --git a/deploy/helm/templates/sandbox-preview-cert.yaml b/deploy/helm/templates/sandbox-preview-cert.yaml index 87d5903587..65427780f3 100644 --- a/deploy/helm/templates/sandbox-preview-cert.yaml +++ b/deploy/helm/templates/sandbox-preview-cert.yaml @@ -20,7 +20,7 @@ metadata: name: {{ $meshServiceName }}-sandbox-preview namespace: {{ $gwNamespace }} labels: - app.kubernetes.io/name: mesh-sandbox-preview + app.kubernetes.io/name: studio-sandbox-preview app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "chart-deco-studio.chart" . }} spec: diff --git a/deploy/helm/templates/sandbox-preview-gateway.yaml b/deploy/helm/templates/sandbox-preview-gateway.yaml index 157783716a..bfbb816978 100644 --- a/deploy/helm/templates/sandbox-preview-gateway.yaml +++ b/deploy/helm/templates/sandbox-preview-gateway.yaml @@ -22,7 +22,7 @@ metadata: name: {{ $meshServiceName }}-sandbox-preview namespace: {{ $gwNamespace }} labels: - app.kubernetes.io/name: mesh-sandbox-preview + app.kubernetes.io/name: studio-sandbox-preview app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "chart-deco-studio.chart" . }} annotations: @@ -61,7 +61,7 @@ metadata: name: {{ $meshServiceName }}-sandbox-preview namespace: {{ .Release.Namespace }} labels: - app.kubernetes.io/name: mesh-sandbox-preview + app.kubernetes.io/name: studio-sandbox-preview app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "chart-deco-studio.chart" . }} spec: diff --git a/deploy/helm/templates/sandbox-rbac.yaml b/deploy/helm/templates/sandbox-rbac.yaml index f6cfdd44ef..b71fc5ec31 100644 --- a/deploy/helm/templates/sandbox-rbac.yaml +++ b/deploy/helm/templates/sandbox-rbac.yaml @@ -14,7 +14,7 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: - name: mesh-sandbox-runner + name: studio-sandbox-runner namespace: agent-sandbox-system labels: {{- include "chart-deco-studio.labels" . | nindent 4 }} @@ -41,7 +41,7 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: - name: mesh-sandbox-runner + name: studio-sandbox-runner namespace: agent-sandbox-system labels: {{- include "chart-deco-studio.labels" . | nindent 4 }} @@ -51,6 +51,6 @@ subjects: namespace: {{ .Release.Namespace }} roleRef: kind: Role - name: mesh-sandbox-runner + name: studio-sandbox-runner apiGroup: rbac.authorization.k8s.io {{- end }} diff --git a/deploy/helm/templates/sandbox-template.yaml b/deploy/helm/templates/sandbox-template.yaml index 2ae6c92d9f..6821ab63e8 100644 --- a/deploy/helm/templates/sandbox-template.yaml +++ b/deploy/helm/templates/sandbox-template.yaml @@ -9,10 +9,10 @@ apiVersion: extensions.agents.x-k8s.io/v1alpha1 kind: SandboxTemplate metadata: - name: mesh-sandbox + name: studio-sandbox namespace: agent-sandbox-system labels: - app.kubernetes.io/name: mesh-sandbox + app.kubernetes.io/name: studio-sandbox app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "chart-deco-studio.chart" . }} spec: @@ -31,8 +31,8 @@ spec: podTemplate: metadata: labels: - app.kubernetes.io/name: mesh-sandbox - # Do NOT set `mesh.decocms.com/role` here. The operator (v0.4.2+) + app.kubernetes.io/name: studio-sandbox + # Do NOT set `studio.decocms.com/role` here. The operator (v0.4.2+) # rejects claims whose additionalPodMetadata defines a label key # already present in the template — even when the values differ — # with "metadata override conflict". The runner sets role=claimed @@ -49,6 +49,10 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.sandbox.kubernetes.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} {{- if not .Values.sandbox.kubernetes.hostUsers }} # User namespace remap: UID 1000 inside the pod maps to a high # subordinate UID on the node, so a container escape lands as a diff --git a/deploy/helm/templates/sandbox-warm-pool.yaml b/deploy/helm/templates/sandbox-warm-pool.yaml index a8947dba4f..887b056fbd 100644 --- a/deploy/helm/templates/sandbox-warm-pool.yaml +++ b/deploy/helm/templates/sandbox-warm-pool.yaml @@ -9,14 +9,14 @@ apiVersion: extensions.agents.x-k8s.io/v1alpha1 kind: SandboxWarmPool metadata: - name: mesh-sandbox + name: studio-sandbox namespace: agent-sandbox-system labels: - app.kubernetes.io/name: mesh-sandbox + app.kubernetes.io/name: studio-sandbox app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "chart-deco-studio.chart" . }} spec: replicas: {{ .Values.sandbox.kubernetes.warmPool.size }} sandboxTemplateRef: - name: mesh-sandbox + name: studio-sandbox {{- end }} diff --git a/deploy/helm/templates/validations.yaml b/deploy/helm/templates/validations.yaml index 7373c68694..87a40b21f5 100644 --- a/deploy/helm/templates/validations.yaml +++ b/deploy/helm/templates/validations.yaml @@ -3,4 +3,5 @@ This file only runs chart-level validations and renders no resources. */ -}} {{- include "chart-deco-studio.validate" . -}} {{- include "chart-deco-studio.validateOtel" . -}} +{{- include "chart-deco-studio.validateSandboxPreviewGateway" . -}} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index fc15c33c5b..d500790978 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -362,7 +362,7 @@ sandbox: # supports) with the per-claim handle. Set in prod once a wildcard # gateway + cert is in place; leave empty in local dev so the runner # falls back to the 127.0.0.1 port-forward URL. Surfaced into the - # mesh container as MESH_SANDBOX_PREVIEW_URL_PATTERN. + # mesh container as STUDIO_SANDBOX_PREVIEW_URL_PATTERN. # # Should match `previewGateway.domain` below — e.g. when # previewGateway.domain="preview.decocms.com", set this to @@ -418,6 +418,20 @@ sandbox: # operator: Equal # value: sandbox # effect: NoSchedule + # Affinity rules merged into the sandbox PodSpec. Use podAffinity to + # co-locate sandbox pods on the same node (cheaper warm-pool packing, + # shared image cache); use nodeAffinity for soft node preferences that + # nodeSelector can't express. Empty default = scheduler picks. + affinity: {} + # affinity: + # podAffinity: + # preferredDuringSchedulingIgnoredDuringExecution: + # - weight: 100 + # podAffinityTerm: + # labelSelector: + # matchLabels: + # app.kubernetes.io/name: studio-sandbox + # topologyKey: kubernetes.io/hostname # User namespace remap (`spec.hostUsers: false`): UID 1000 inside the # pod maps to a high, unprivileged subordinate UID on the node, so a # container escape doesn't land as a real node UID. Requires K8s diff --git a/deploy/k8s-sandbox/local/README.md b/deploy/k8s-sandbox/local/README.md index 31e675fea9..b6bc3eb7a6 100644 --- a/deploy/k8s-sandbox/local/README.md +++ b/deploy/k8s-sandbox/local/README.md @@ -28,7 +28,7 @@ Pins: - agent-sandbox operator: `v0.4.2` (matches prod; hardcoded in `up.sh`) - kube-prometheus-stack: `65.5.1` - opentelemetry-collector: `0.108.0` -- cluster name: `mesh-sandbox-dev` +- cluster name: `studio-sandbox-dev` - namespace: `agent-sandbox-system` (sandboxes), `monitoring` (Prom/Grafana/OTel) - image tag: `mesh-sandbox:local` @@ -59,7 +59,7 @@ Pins: scrapes per-node kubelet, enriches with tenant labels, and exposes `/metrics` for Prometheus. Skip with `MONITORING=0 ./up.sh`. -All `kubectl` calls pass `--context kind-mesh-sandbox-dev` so an ambient +All `kubectl` calls pass `--context kind-studio-sandbox-dev` so an ambient `KUBECONFIG` can't accidentally hit a real cluster. ## Local Grafana @@ -67,10 +67,10 @@ All `kubectl` calls pass `--context kind-mesh-sandbox-dev` so an ambient After `up.sh`: ```bash -kubectl --context kind-mesh-sandbox-dev port-forward \ +kubectl --context kind-studio-sandbox-dev port-forward \ -n monitoring svc/kube-prometheus-stack-grafana 3001:80 # → http://localhost:3001 (admin / admin) -# → Dashboards → "Mesh Sandbox Overview" +# → Dashboards → "Studio Sandbox Overview" ``` Dashboard panels (per-org, per-sandbox-handle): @@ -87,7 +87,7 @@ The pipeline: kubelet (cAdvisor) ──► OTel collector daemonset │ - kubeletstats receiver │ - k8sattributes processor (reads pod labels: - │ mesh.decocms.com/{org-id,user-id,sandbox-handle,role} + │ studio.decocms.com/{org-id,user-id,sandbox-handle,role} │ → series labels: org_id, user_id, sandbox_handle, sandbox_role) │ - prometheus exporter on :8889 ▼ @@ -99,8 +99,8 @@ populated in `KubernetesSandboxRunner.provision()` from the `tenant` field on `EnsureOptions`. Verify they're landing: ```bash -kubectl --context kind-mesh-sandbox-dev \ - get pod -n agent-sandbox-system --show-labels | grep mesh.decocms.com +kubectl --context kind-studio-sandbox-dev \ + get pod -n agent-sandbox-system --show-labels | grep studio.decocms.com ``` To iterate on dashboards/values without rebuilding the cluster: @@ -140,7 +140,7 @@ has to carry one — any string works, just keep it consistent with the curl call below. ```bash -CTX=kind-mesh-sandbox-dev +CTX=kind-studio-sandbox-dev TOKEN="smoke-$(openssl rand -hex 16)" cat < { const sandboxA = await runnerA.ensure(ID, {}); handle = sandboxA.handle; log(" created", `handle=${handle} (${Date.now() - t0}ms)`); - if (!handle.startsWith("mesh-sb-")) { + if (!handle.startsWith("studio-sb-")) { throw new Error(`handle missing expected prefix: ${handle}`); } diff --git a/deploy/k8s-sandbox/local/up.sh b/deploy/k8s-sandbox/local/up.sh index 9472cbafc7..3819d5a040 100755 --- a/deploy/k8s-sandbox/local/up.sh +++ b/deploy/k8s-sandbox/local/up.sh @@ -2,7 +2,7 @@ # Bring up the local kind cluster used by KubernetesSandboxRunner. # # Idempotent: re-running re-applies the operator + template and reloads the -# sandbox image. Cluster creation is skipped if mesh-sandbox-dev already +# sandbox image. Cluster creation is skipped if studio-sandbox-dev already # exists. # # Pins agent-sandbox to v0.4.2 (matches prod in @@ -10,7 +10,7 @@ # Bumping here means bumping prod too. set -euo pipefail -CLUSTER_NAME="mesh-sandbox-dev" +CLUSTER_NAME="studio-sandbox-dev" OPERATOR_VERSION="v0.4.2" IMAGE_TAG="mesh-sandbox:local" @@ -116,15 +116,15 @@ if [[ "${MONITORING:-1}" == "1" ]]; then log "applying sandbox dashboard ConfigMap" # `--dry-run | apply` so re-runs replace the ConfigMap idempotently. - kubectl --context "${KCTX}" -n monitoring create configmap mesh-sandbox-dashboard \ + kubectl --context "${KCTX}" -n monitoring create configmap studio-sandbox-dashboard \ --from-file="${MONITORING_DIR}/dashboards/sandbox-overview.json" \ --dry-run=client -o yaml | \ kubectl --context "${KCTX}" apply -f - - kubectl --context "${KCTX}" -n monitoring label configmap mesh-sandbox-dashboard \ + kubectl --context "${KCTX}" -n monitoring label configmap studio-sandbox-dashboard \ grafana_dashboard=1 --overwrite >/dev/null log "monitoring ready: kubectl port-forward -n monitoring svc/kube-prometheus-stack-grafana 3001:80" - log " → http://localhost:3001 (admin / admin) → Dashboards → 'Mesh Sandbox Overview'" + log " → http://localhost:3001 (admin / admin) → Dashboards → 'Studio Sandbox Overview'" fi fi diff --git a/deploy/k8s-sandbox/monitoring/README.md b/deploy/k8s-sandbox/monitoring/README.md index 095ac58bd2..d8a2119c03 100644 --- a/deploy/k8s-sandbox/monitoring/README.md +++ b/deploy/k8s-sandbox/monitoring/README.md @@ -6,7 +6,7 @@ Per-org / per-sandbox cost-attribution metrics. Pipeline: kubelet (cAdvisor) ──► OTel collector daemonset │ - kubeletstats receiver │ - k8sattributes processor (reads pod labels: - │ mesh.decocms.com/{org-id,user-id,sandbox-handle,role} + │ studio.decocms.com/{org-id,user-id,sandbox-handle,role} │ → series labels: org_id, user_id, sandbox_handle, sandbox_role) │ - prometheus exporter on :8889 ▼ @@ -57,10 +57,10 @@ helm upgrade --install otel-collector-sandbox \ --wait # 3. Grafana dashboard ConfigMap (sidecar auto-imports it) -kubectl -n monitoring create configmap mesh-sandbox-dashboard \ +kubectl -n monitoring create configmap studio-sandbox-dashboard \ --from-file=deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json \ --dry-run=client -o yaml | kubectl apply -f - -kubectl -n monitoring label configmap mesh-sandbox-dashboard grafana_dashboard=1 --overwrite +kubectl -n monitoring label configmap studio-sandbox-dashboard grafana_dashboard=1 --overwrite ``` ## Prod overlay examples @@ -123,7 +123,7 @@ config: Confirm tenant labels are landing on sandbox pods: ```bash -kubectl get pod -n agent-sandbox-system --show-labels | grep mesh.decocms.com +kubectl get pod -n agent-sandbox-system --show-labels | grep studio.decocms.com ``` Confirm the collector's `/metrics` endpoint is being scraped: in the diff --git a/deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json b/deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json index d184eb58bb..22d9ea7a3b 100644 --- a/deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json +++ b/deploy/k8s-sandbox/monitoring/dashboards/sandbox-overview.json @@ -1,11 +1,11 @@ { - "title": "Mesh Sandbox Overview", - "uid": "mesh-sandbox-overview", + "title": "Studio Sandbox Overview", + "uid": "studio-sandbox-overview", "schemaVersion": 39, "version": 1, "refresh": "30s", "time": { "from": "now-1h", "to": "now" }, - "tags": ["mesh", "sandbox"], + "tags": ["studio", "sandbox"], "templating": { "list": [ { @@ -77,7 +77,7 @@ }, "targets": [ { - "expr": "count(k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",app_name=\"mesh-sandbox\",sandbox_handle=\"\"}) or vector(0)", + "expr": "count(k8s_pod_cpu_utilization_ratio{k8s_namespace_name=\"$namespace\",app_name=\"studio-sandbox\",sandbox_handle=\"\"}) or vector(0)", "refId": "A", "instant": true, "range": false @@ -173,7 +173,7 @@ "fieldConfig": { "defaults": { "unit": "ops" } }, "targets": [ { - "expr": "sum by (outcome) (rate(mesh_sandbox_ensure_outcome_total{runner_kind=\"kubernetes\"}[5m]))", + "expr": "sum by (outcome) (rate(studio_sandbox_ensure_outcome_total{runner_kind=\"kubernetes\"}[5m]))", "legendFormat": "{{outcome}}", "refId": "A" } @@ -188,7 +188,7 @@ "fieldConfig": { "defaults": { "unit": "ms" } }, "targets": [ { - "expr": "histogram_quantile(0.95, sum by (le, source) (rate(mesh_sandbox_proxy_duration_ms_bucket{runner_kind=\"kubernetes\"}[5m])))", + "expr": "histogram_quantile(0.95, sum by (le, source) (rate(studio_sandbox_proxy_duration_ms_bucket{runner_kind=\"kubernetes\"}[5m])))", "legendFormat": "{{source}}", "refId": "A" } @@ -210,7 +210,7 @@ "fieldConfig": { "defaults": { "unit": "none" } }, "targets": [ { - "expr": "sum(mesh_sandbox_active{runner_kind=\"kubernetes\"})", + "expr": "sum(studio_sandbox_active{runner_kind=\"kubernetes\"})", "legendFormat": "mesh", "refId": "A" }, diff --git a/deploy/k8s-sandbox/monitoring/values-otel-collector.yaml b/deploy/k8s-sandbox/monitoring/values-otel-collector.yaml index d79d2cad25..bb74c3227a 100644 --- a/deploy/k8s-sandbox/monitoring/values-otel-collector.yaml +++ b/deploy/k8s-sandbox/monitoring/values-otel-collector.yaml @@ -116,18 +116,18 @@ config: - k8s.node.name labels: - tag_name: org_id - key: mesh.decocms.com/org-id + key: studio.decocms.com/org-id from: pod - tag_name: user_id - key: mesh.decocms.com/user-id + key: studio.decocms.com/user-id from: pod - tag_name: sandbox_handle - key: mesh.decocms.com/sandbox-handle + key: studio.decocms.com/sandbox-handle from: pod - tag_name: sandbox_role - key: mesh.decocms.com/role + key: studio.decocms.com/role from: pod - # `app.kubernetes.io/name=mesh-sandbox` is set by the SandboxTemplate + # `app.kubernetes.io/name=studio-sandbox` is set by the SandboxTemplate # on every sandbox pod (warm-pool + claimed). Lets the warm-pool # query distinguish sandbox pods from operator/other pods in the # namespace by app_name + absence-of-handle. diff --git a/packages/sandbox/server/runner/k8s/client.test.ts b/packages/sandbox/server/runner/k8s/client.test.ts index b05cf16b76..10cbba7cef 100644 --- a/packages/sandbox/server/runner/k8s/client.test.ts +++ b/packages/sandbox/server/runner/k8s/client.test.ts @@ -104,7 +104,7 @@ function makeClaim(name: string): SandboxClaim { kind: "SandboxClaim", metadata: { name, namespace: NS }, spec: { - sandboxTemplateRef: { name: "mesh-sandbox" }, + sandboxTemplateRef: { name: "studio-sandbox" }, lifecycle: { shutdownPolicy: "Delete" }, }, }; @@ -115,7 +115,7 @@ function makeClaim(name: string): SandboxClaim { describe("createSandboxClaim", () => { it("POSTs the claim body verbatim to the plural endpoint", async () => { fetchImpl = async () => jsonResponse(201, { kind: "SandboxClaim" }); - const claim = makeClaim("mesh-sb-abc"); + const claim = makeClaim("studio-sb-abc"); await createSandboxClaim(makeKc(), NS, claim); expect(fetchCalls).toHaveLength(1); @@ -139,9 +139,9 @@ describe("createSandboxClaim", () => { const claim: SandboxClaim = { apiVersion: `${K8S_CONSTANTS.CLAIM_API_GROUP}/${K8S_CONSTANTS.CLAIM_API_VERSION}`, kind: "SandboxClaim", - metadata: { name: "mesh-sb-tok", namespace: NS }, + metadata: { name: "studio-sb-tok", namespace: NS }, spec: { - sandboxTemplateRef: { name: "mesh-sandbox" }, + sandboxTemplateRef: { name: "studio-sandbox" }, env: [{ name: "DAEMON_TOKEN", value: "abc123" }], warmpool: "none", lifecycle: { shutdownPolicy: "Delete" }, @@ -230,7 +230,7 @@ describe("patchSandboxClaimShutdown", () => { await patchSandboxClaimShutdown( makeKc(), NS, - "mesh-sb-x", + "studio-sb-x", "2026-04-01T12:00:00.000Z", ); expect(fetchCalls).toHaveLength(1); diff --git a/packages/sandbox/server/runner/k8s/runner.ts b/packages/sandbox/server/runner/k8s/runner.ts index 20695c5d13..b143fbb5de 100644 --- a/packages/sandbox/server/runner/k8s/runner.ts +++ b/packages/sandbox/server/runner/k8s/runner.ts @@ -77,7 +77,7 @@ const LOG_LABEL = "KubernetesSandboxRunner"; // Shared-namespace topology for MVP; tenancy enforced by unguessable claim // names (sha256(userId:projectRef)). Per-org namespaces are deferred. const DEFAULT_NAMESPACE = "agent-sandbox-system"; -const DEFAULT_TEMPLATE_NAME = "mesh-sandbox"; +const DEFAULT_TEMPLATE_NAME = "studio-sandbox"; const DAEMON_CONTAINER_PORT = 9000; // In-pod port the daemon's reverse proxy targets. Mesh never connects here @@ -117,7 +117,7 @@ const RESERVED_ENV_KEYS = new Set([ const DEFAULT_IDLE_TTL_MS = 15 * 60 * 1000; /** Handle prefix + 16-hex hash = 24 chars, well under K8s's 63-char label cap. */ -export const HANDLE_PREFIX = "mesh-sb-"; +export const HANDLE_PREFIX = "studio-sb-"; const HANDLE_HASH_LEN = 16; /** @@ -205,6 +205,15 @@ interface K8sRecord { * adopt fallback when claim labels were absent). */ tenant: RunnerTenant | null; + /** + * The original options the caller passed to `ensure()`. Persisted so + * `resurrectByHandle` can re-provision an evicted sandbox autonomously + * (15-min idle TTL deletes the claim — without these we'd come back as + * an empty pod with no repo cloned). Null on adopt paths where we can't + * recover the original opts; resurrection falls back to throwing/404 in + * that case so the caller's normal VM_START flow can repopulate them. + */ + ensureOpts: EnsureOptions | null; } interface PersistedK8sState { @@ -214,6 +223,14 @@ interface PersistedK8sState { workload?: Workload | null; daemonBootId?: string; tenant?: RunnerTenant | null; + /** + * Original `EnsureOptions`. Persisted so `resurrectByHandle` can re-ensure + * after the operator deletes the claim on idle TTL. Optional for + * back-compat: rows written before this field existed lack it; resurrection + * returns null in that case and the caller surfaces 404 (UI's existing + * VM_START reprovision flow then runs with full opts). + */ + ensureOpts?: EnsureOptions; [k: string]: unknown; } @@ -386,11 +403,21 @@ export class KubernetesSandboxRunner implements SandboxRunner { */ async resolvePreviewUpstreamUrl(handle: string): Promise { if (this.previewUrlPattern) { + // Production mode: synthesize the in-cluster Service URL from the + // handle. We deliberately don't pre-validate that the claim is still + // alive — every preview request would pay a K8s API call. When the + // sandbox has been evicted, the downstream fetch fails and + // `proxyPreviewRequest` catches it + drives resurrection from there. return `http://${handle}.${this.namespace}.svc.cluster.local:${DAEMON_CONTAINER_PORT}`; } const rec = await this.getRecord(handle); - if (!rec) return null; - return rec.daemonUrl; + if (rec) return rec.daemonUrl; + // Dev mode: cold cache + state-store miss. Try resurrection before + // surfacing 404 — the pod may have been operator-evicted on idle TTL + // and the caller (preview iframe, SSE EventSource probe) needs the + // sandbox back to make any progress. + const resurrected = await this.resurrectByHandle(handle); + return resurrected ? resurrected.daemonUrl : null; } /** @@ -438,7 +465,8 @@ export class KubernetesSandboxRunner implements SandboxRunner { return jsonResponse(404, { error: "not found" }); } - const target = `${upstreamBase}${reqUrl.pathname}${reqUrl.search}`; + const reqTarget = (base: string) => + `${base}${reqUrl.pathname}${reqUrl.search}`; const headers = new Headers(request.headers); for (const h of PREVIEW_STRIP_REQUEST_HEADERS) headers.delete(h); @@ -454,7 +482,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { let upstream: Response; try { - upstream = await fetch(target, init as RequestInit); + upstream = await fetch(reqTarget(upstreamBase), init as RequestInit); } catch (err) { // Truncate to host+pathname — query strings can carry secrets // (magic-link tokens, signed URLs) and would otherwise end up in @@ -463,6 +491,56 @@ export class KubernetesSandboxRunner implements SandboxRunner { console.warn( `[${LOG_LABEL}] preview fetch to ${safeTarget} failed: ${err instanceof Error ? err.message : String(err)}`, ); + + // Recover from operator-driven eviction (15-min idle TTL): the + // claim + Service are gone but our records cache (or the + // synthesized prod-mode URL) still pointed at the stale endpoint. + // Drop the cache and resurrect via state-store. Retry only for + // replay-safe methods — `init.body` is a stream that's been + // consumed by the failed fetch; replaying a POST would silently + // send an empty body. The browser/caller can retry the mutating + // request after this 502 surfaces; the resurrected sandbox will + // be ready for that next attempt. + if (request.method === "GET" || request.method === "HEAD") { + this.invalidateRecord(handle); + const resurrected = await this.resurrectByHandle(handle).catch( + () => null, + ); + if (resurrected) { + const retryBase = await this.resolvePreviewUpstreamUrl(handle); + if (retryBase) { + try { + upstream = await fetch( + reqTarget(retryBase), + init as RequestInit, + ); + const responseHeaders = new Headers(); + for (const [k, v] of upstream.headers.entries()) { + if ( + !PREVIEW_STRIP_RESPONSE_HEADERS.includes(k.toLowerCase()) + ) { + responseHeaders.set(k, v); + } + } + status = upstream.status; + return new Response(upstream.body, { + status: upstream.status, + statusText: upstream.statusText, + headers: responseHeaders, + }); + } catch (retryErr) { + console.warn( + `[${LOG_LABEL}] preview fetch retry to ${safeTarget} failed: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`, + ); + } + } + } + } else { + // Non-replay-safe method: still drop the stale cache so the next + // request goes through fresh validation. + this.invalidateRecord(handle); + } + status = 502; return jsonResponse(502, { error: "sandbox daemon unreachable" }); } @@ -663,8 +741,8 @@ export class KubernetesSandboxRunner implements SandboxRunner { // adopt path can recover orgId/userId after a state-store wipe; // adopt() reads claim.metadata.labels, not pod labels. labels: { - "app.kubernetes.io/name": "mesh-sandbox", - "app.kubernetes.io/managed-by": "mesh", + "app.kubernetes.io/name": "studio-sandbox", + "app.kubernetes.io/managed-by": "studio", ...buildTenantLabels(opts.tenant), }, }, @@ -746,6 +824,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { workload: opts.workload ?? null, daemonBootId, tenant: opts.tenant ?? null, + ensureOpts: stripEnsureOpts(opts), }; } @@ -796,6 +875,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { workload: state.workload ?? null, daemonBootId: live.bootId, tenant: state.tenant ?? null, + ensureOpts: state.ensureOpts ?? null, }; } @@ -827,6 +907,13 @@ export class KubernetesSandboxRunner implements SandboxRunner { // claim pre-dates tenant labelling (back-compat with already-running // sandboxes when this code rolls out). tenant: readClaimTenant(claim), + // Adopt happens when the state-store is empty but a claim with our + // deterministic name still exists in the cluster (e.g. mesh restart + // without state-store, or state-store wipe). The original opts aren't + // recoverable from the claim alone, so resurrection on this record + // can't autonomously re-provision; falls back to the caller's + // VM_START path. + ensureOpts: null, }; } @@ -874,10 +961,53 @@ export class KubernetesSandboxRunner implements SandboxRunner { return rec; } + /** + * Re-ensure a sandbox after operator-driven eviction (15-min idle TTL deletes + * claim + pod). Looks up the SandboxId from the state-store by handle, then + * runs the standard `ensure()` path with the persisted `EnsureOptions` so the + * fresh provision rehydrates with the same repo/env/workload. + * + * Returns null when: + * - no state-store (test runners) — caller surfaces 404, + * - handle has no row (truly unknown) — caller surfaces 404, + * - row predates `ensureOpts` persistence (back-compat: rows from before + * this change). Resurrecting with empty opts would create an empty pod + * with no repo cloned, which is worse than 404. UI's existing + * notFound→VM_START flow re-supplies opts in that case. + */ + private async resurrectByHandle(handle: string): Promise { + if (!this.stateStore) return null; + const row = await this.stateStore.getByHandle(RUNNER_KIND, handle); + if (!row) return null; + const persistedOpts = (row.state as Partial).ensureOpts; + if (!persistedOpts) return null; + // ensure() is idempotent + advisory-locked, so concurrent resurrections + // for the same handle collapse to a single provision. The lock is keyed + // on (userId, projectRef, kind), the same identity our state-store row + // is keyed on. + await this.ensure(row.id, persistedOpts); + return this.records.get(handle) ?? null; + } + private async requireRecord(handle: string): Promise { const rec = await this.getRecord(handle); - if (!rec) throw new Error(`unknown sandbox handle ${handle}`); - return rec; + if (rec) return rec; + const resurrected = await this.resurrectByHandle(handle); + if (resurrected) return resurrected; + throw new Error(`unknown sandbox handle ${handle}`); + } + + /** + * Drop the in-memory record cache for `handle`. Called when the cached + * `daemonUrl` proves stale (e.g. fetch fails with connection refused after + * the operator deleted the underlying pod). The next access goes through + * the state-store + rehydrate or resurrection path. + */ + private invalidateRecord(handle: string): void { + const rec = this.records.get(handle); + if (!rec) return; + this.records.delete(handle); + this.closeForwarder(rec.daemonForward); } // ---- Metric helpers ------------------------------------------------------- @@ -940,6 +1070,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { workload: rec.workload, daemonBootId: rec.daemonBootId, tenant: rec.tenant, + ...(rec.ensureOpts ? { ensureOpts: rec.ensureOpts } : {}), }; await ops.put(rec.id, RUNNER_KIND, { handle: rec.handle, state }); } @@ -1086,17 +1217,17 @@ interface RunnerMetrics { function buildRunnerMetrics(meter: Meter): RunnerMetrics { return { - active: meter.createUpDownCounter("mesh.sandbox.active", { + active: meter.createUpDownCounter("studio.sandbox.active", { description: "Active sandbox count, by runner kind and owning org. Cross-checks the cAdvisor-derived count from the cluster — divergence between the two indicates orphaned claims (mesh deleted but K8s didn't reap) or unattributed pods.", unit: "{sandbox}", }), - ensureOutcome: meter.createCounter("mesh.sandbox.ensure.outcome", { + ensureOutcome: meter.createCounter("studio.sandbox.ensure.outcome", { description: "Outcome of each ensure() call: fresh provision, resume from state-store after restart, or adopt of a cluster-side claim mesh didn't know about. Cold-start ratio is the primary input for warm-pool sizing.", unit: "{call}", }), - proxyDurationMs: meter.createHistogram("mesh.sandbox.proxy.duration_ms", { + proxyDurationMs: meter.createHistogram("studio.sandbox.proxy.duration_ms", { description: "Wall-clock latency of mesh-mediated requests to the sandbox daemon: tool exec proxies (source=daemon) and preview iframe traffic (source=preview).", unit: "ms", @@ -1162,10 +1293,10 @@ function jsonResponse(status: number, body: unknown): Response { // K8s label keys mesh attaches. Centralized so writers (buildTenantLabels) // and the reader (readClaimTenant) can't drift. const LABEL_KEYS = { - role: "mesh.decocms.com/role", - sandboxHandle: "mesh.decocms.com/sandbox-handle", - orgId: "mesh.decocms.com/org-id", - userId: "mesh.decocms.com/user-id", + role: "studio.decocms.com/role", + sandboxHandle: "studio.decocms.com/sandbox-handle", + orgId: "studio.decocms.com/org-id", + userId: "studio.decocms.com/user-id", } as const; // K8s label values: ≤63 chars, must match `(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?`. @@ -1228,6 +1359,20 @@ function tenantAttrs(tenant: RunnerTenant | null): { }; } +/** + * Subset of `EnsureOptions` worth persisting for resurrection. Drops `image` + * (k8s ignores it — template pins the image) and any nullish entries so the + * persisted blob stays small. + */ +function stripEnsureOpts(opts: EnsureOptions): EnsureOptions | null { + const out: EnsureOptions = {}; + if (opts.repo) out.repo = opts.repo; + if (opts.workload) out.workload = opts.workload; + if (opts.env && Object.keys(opts.env).length > 0) out.env = opts.env; + if (opts.tenant) out.tenant = opts.tenant; + return Object.keys(out).length > 0 ? out : null; +} + /** Fallback for when callers don't provide `repo.displayName`. */ function deriveRepoLabel(cloneUrl: string): string { try { From 34ada690ff0f4b14565c28d13ed6382d8666f904 Mon Sep 17 00:00:00 2001 From: pedrofrxncx Date: Tue, 28 Apr 2026 17:47:33 -0300 Subject: [PATCH 16/16] Refactor sandbox runner references from Kubernetes to agent-sandbox - Updated environment variable references and type definitions to replace `MESH_SANDBOX_RUNNER` and `kubernetes` with `STUDIO_SANDBOX_RUNNER` and `agent-sandbox` across multiple files. - Adjusted comments and documentation to reflect the new naming convention for the agent-sandbox runner. - Enhanced test cases and configurations to ensure consistency with the updated sandbox runner implementation. These changes improve clarity and maintainability in the codebase regarding the sandbox environment. --- .../routes/decopilot/built-in-tools/index.ts | 2 +- .../src/api/routes/decopilot/stream-core.ts | 4 +-- apps/mesh/src/cli/build-child-env.ts | 2 +- apps/mesh/src/index.ts | 8 ++--- apps/mesh/src/sandbox/lifecycle.ts | 12 ++++---- apps/mesh/src/sandbox/preview-proxy.ts | 10 +++---- apps/mesh/src/tools/vm/start.test.ts | 12 ++++---- apps/mesh/src/tools/vm/start.ts | 2 +- apps/mesh/src/tools/vm/stop.test.ts | 12 ++++---- apps/mesh/src/tools/vm/stop.ts | 2 +- apps/mesh/src/web/components/vm/env/env.tsx | 2 +- .../web/components/vm/hooks/use-vm-start.ts | 2 +- deploy/helm/Chart.lock | 4 +-- deploy/helm/Chart.yaml | 2 +- deploy/helm/README.md | 28 +++++++++--------- deploy/helm/charts/agent-sandbox-0.1.0.tgz | Bin 23805 -> 24096 bytes deploy/helm/charts/agent-sandbox/values.yaml | 2 +- deploy/helm/examples/values-kind.yaml | 10 +++---- deploy/helm/templates/_helpers.tpl | 6 ++-- deploy/helm/templates/configmap.yaml | 8 ++--- .../templates/sandbox-network-policy.yaml | 4 +-- .../helm/templates/sandbox-preview-cert.yaml | 10 +++---- .../templates/sandbox-preview-gateway.yaml | 12 ++++---- deploy/helm/templates/sandbox-rbac.yaml | 4 +-- deploy/helm/templates/sandbox-template.yaml | 28 +++++++++--------- deploy/helm/templates/sandbox-warm-pool.yaml | 4 +-- deploy/helm/values.yaml | 4 +-- deploy/k8s-sandbox/local/README.md | 6 ++-- deploy/k8s-sandbox/local/smoke.ts | 10 +++---- deploy/k8s-sandbox/local/up.sh | 2 +- deploy/k8s-sandbox/monitoring/README.md | 2 +- packages/mesh-sdk/src/types/virtual-mcp.ts | 4 +-- packages/sandbox/README.md | 14 ++++----- packages/sandbox/package.json | 2 +- .../{k8s => agent-sandbox}/client.test.ts | 0 .../runner/{k8s => agent-sandbox}/client.ts | 0 .../{k8s => agent-sandbox}/constants.ts | 0 .../runner/{k8s => agent-sandbox}/index.ts | 4 +-- .../runner/{k8s => agent-sandbox}/runner.ts | 12 ++++---- packages/sandbox/server/runner/index.ts | 24 +++++++-------- packages/sandbox/server/runner/types.ts | 2 +- 41 files changed, 139 insertions(+), 139 deletions(-) rename packages/sandbox/server/runner/{k8s => agent-sandbox}/client.test.ts (100%) rename packages/sandbox/server/runner/{k8s => agent-sandbox}/client.ts (100%) rename packages/sandbox/server/runner/{k8s => agent-sandbox}/constants.ts (100%) rename packages/sandbox/server/runner/{k8s => agent-sandbox}/index.ts (81%) rename packages/sandbox/server/runner/{k8s => agent-sandbox}/runner.ts (99%) diff --git a/apps/mesh/src/api/routes/decopilot/built-in-tools/index.ts b/apps/mesh/src/api/routes/decopilot/built-in-tools/index.ts index 6240f7c42f..01c01617c4 100644 --- a/apps/mesh/src/api/routes/decopilot/built-in-tools/index.ts +++ b/apps/mesh/src/api/routes/decopilot/built-in-tools/index.ts @@ -132,7 +132,7 @@ async function buildAllTools( // VM file tools — same six LLM-visible tools across runners (schemas in // vm-tools/schemas.ts). Dispatch resolves through `getRunnerByKind` so // the entry's recorded runnerKind drives the routing, regardless of the - // current MESH_SANDBOX_RUNNER env value. When no entry exists, fall back + // current STUDIO_SANDBOX_RUNNER env value. When no entry exists, fall back // to the QuickJS `sandbox` tool — VM_START must run first for file tools. const vmNeedsApproval = toolNeedsApproval(toolApprovalLevel, false, approvalOpts) !== false; diff --git a/apps/mesh/src/api/routes/decopilot/stream-core.ts b/apps/mesh/src/api/routes/decopilot/stream-core.ts index 36f31b6d89..1dd74b1941 100644 --- a/apps/mesh/src/api/routes/decopilot/stream-core.ts +++ b/apps/mesh/src/api/routes/decopilot/stream-core.ts @@ -476,7 +476,7 @@ async function streamCoreInner( { vmId: string; previewUrl: string; - runnerKind?: "docker" | "freestyle" | "kubernetes"; + runnerKind?: "docker" | "freestyle" | "agent-sandbox"; } > >; @@ -491,7 +491,7 @@ async function streamCoreInner( runnerKind: (activeVmEntry.runnerKind ?? "freestyle") as | "docker" | "freestyle" - | "kubernetes", + | "agent-sandbox", vmId: activeVmEntry.vmId, } : null; diff --git a/apps/mesh/src/cli/build-child-env.ts b/apps/mesh/src/cli/build-child-env.ts index b8b6796422..af1deaffb7 100644 --- a/apps/mesh/src/cli/build-child-env.ts +++ b/apps/mesh/src/cli/build-child-env.ts @@ -77,7 +77,7 @@ export function buildChildEnv( FIRECRAWL_API_KEY: settings.firecrawlApiKey, // Sandbox runner: read from env by resolveRunnerKindFromEnv() in workers - MESH_SANDBOX_RUNNER: process.env.MESH_SANDBOX_RUNNER, + STUDIO_SANDBOX_RUNNER: process.env.STUDIO_SANDBOX_RUNNER, FREESTYLE_API_KEY: process.env.FREESTYLE_API_KEY, // Browserless diff --git a/apps/mesh/src/index.ts b/apps/mesh/src/index.ts index 99b7f520d5..37c9bed67c 100644 --- a/apps/mesh/src/index.ts +++ b/apps/mesh/src/index.ts @@ -78,7 +78,7 @@ function withSecurityHeaders(res: Response): Response { // Closed early in gracefulShutdown so the port frees before the Hono drain. let ingressServers: import("node:net").Server[] = []; -// Sandbox preview reverse-proxy (K8s only). The base domain is parsed at +// Sandbox preview reverse-proxy (agent-sandbox only). The base domain is parsed at // boot from STUDIO_SANDBOX_PREVIEW_URL_PATTERN; null disables the proxy and // preview-host requests fall through to the normal mesh routing (which 404s // because nothing matches). The Bun-level WS handler is registered @@ -100,10 +100,10 @@ const previewProxyDeps = { baseDomain: previewBaseDomain ?? "", getRunner: async () => { const runner = await getOrInitRunnerForPreview(); - if (!runner || runner.kind !== "kubernetes") return null; - // The K8s runner is the only one that exposes proxyPreviewRequest / + if (!runner || runner.kind !== "agent-sandbox") return null; + // The agent-sandbox runner is the only one that exposes proxyPreviewRequest / // resolvePreviewUpstreamUrl; cast is safe after the kind check. - return runner as unknown as import("@decocms/sandbox/runner/k8s").KubernetesSandboxRunner; + return runner as unknown as import("@decocms/sandbox/runner/agent-sandbox").AgentSandboxRunner; }, }; diff --git a/apps/mesh/src/sandbox/lifecycle.ts b/apps/mesh/src/sandbox/lifecycle.ts index 9cb1fd3d72..24068b7dc1 100644 --- a/apps/mesh/src/sandbox/lifecycle.ts +++ b/apps/mesh/src/sandbox/lifecycle.ts @@ -1,6 +1,6 @@ /** * Runner singletons, one per kind. VM_DELETE dispatches on the entry's - * recorded runnerKind (not env), so a pod that flipped MESH_SANDBOX_RUNNER + * recorded runnerKind (not env), so a pod that flipped STUDIO_SANDBOX_RUNNER * between start and stop still tears down the right kind of VM. * Boot/shutdown sweeps are Docker-only — other runners' sandboxes outlive * mesh by design, so a generic sweep would nuke active user VMs. @@ -73,17 +73,17 @@ async function instantiate( ); return new FreestyleSandboxRunner({ stateStore }); } - case "kubernetes": { + case "agent-sandbox": { // Dynamic import — @kubernetes/client-node is heavy and only needed - // when MESH_SANDBOX_RUNNER=kubernetes. Docker/Freestyle deploys never + // when STUDIO_SANDBOX_RUNNER=agent-sandbox. Docker/Freestyle deploys never // load it. - const { KubernetesSandboxRunner } = await import( - "@decocms/sandbox/runner/k8s" + const { AgentSandboxRunner } = await import( + "@decocms/sandbox/runner/agent-sandbox" ); // `meter` is reassigned by initObservability() after sdk.start(); read // it at runner construction (post-init) so we get the real instruments // not the no-op evaluated at module load. - return new KubernetesSandboxRunner({ + return new AgentSandboxRunner({ stateStore, previewUrlPattern, meter, diff --git a/apps/mesh/src/sandbox/preview-proxy.ts b/apps/mesh/src/sandbox/preview-proxy.ts index 74d7a81465..43d544c815 100644 --- a/apps/mesh/src/sandbox/preview-proxy.ts +++ b/apps/mesh/src/sandbox/preview-proxy.ts @@ -19,8 +19,8 @@ import { HANDLE_PREFIX, - type KubernetesSandboxRunner, -} from "@decocms/sandbox/runner/k8s"; + type AgentSandboxRunner, +} from "@decocms/sandbox/runner/agent-sandbox"; /** * Cap on frames buffered between client upgrade and upstream WS open. Vite @@ -96,10 +96,10 @@ export function extractHandleFromHost( export interface PreviewProxyDeps { /** * Lazy runner accessor. Returns null when the mesh isn't configured for - * the K8s runner — the caller treats null as "not a preview deployment" - * and falls through. + * the agent-sandbox runner — the caller treats null as "not a preview + * deployment" and falls through. */ - getRunner: () => Promise; + getRunner: () => Promise; baseDomain: string; } diff --git a/apps/mesh/src/tools/vm/start.test.ts b/apps/mesh/src/tools/vm/start.test.ts index 7258bbb332..52fa226db0 100644 --- a/apps/mesh/src/tools/vm/start.test.ts +++ b/apps/mesh/src/tools/vm/start.test.ts @@ -9,9 +9,9 @@ import type { } from "@decocms/sandbox/runner"; import { composeSandboxRef } from "@decocms/sandbox/runner"; -// Pin runner kind — the dev env flips MESH_SANDBOX_RUNNER and VM_START +// Pin runner kind — the dev env flips STUDIO_SANDBOX_RUNNER and VM_START // reads it at handler time. -process.env.MESH_SANDBOX_RUNNER = "freestyle"; +process.env.STUDIO_SANDBOX_RUNNER = "freestyle"; // Mock runner BEFORE importing VM_START — handler is runner-agnostic // and we don't want to pull the real freestyle SDK. @@ -517,8 +517,8 @@ describe("VM_START", () => { }); it("skips freestyle teardown on runner flip — freestyle idles out on its own", async () => { - const original = process.env.MESH_SANDBOX_RUNNER; - process.env.MESH_SANDBOX_RUNNER = "docker"; + const original = process.env.STUDIO_SANDBOX_RUNNER; + process.env.STUDIO_SANDBOX_RUNNER = "docker"; try { const staleEntry: VmMapEntry = { vmId: "mh3fx1hmxzdz1h1agx4m", @@ -543,8 +543,8 @@ describe("VM_START", () => { expect(result.runnerKind).toBe("docker"); expect(result.isNewVm).toBe(true); } finally { - if (original === undefined) delete process.env.MESH_SANDBOX_RUNNER; - else process.env.MESH_SANDBOX_RUNNER = original; + if (original === undefined) delete process.env.STUDIO_SANDBOX_RUNNER; + else process.env.STUDIO_SANDBOX_RUNNER = original; } }); diff --git a/apps/mesh/src/tools/vm/start.ts b/apps/mesh/src/tools/vm/start.ts index 6e7eaa0710..331865dc46 100644 --- a/apps/mesh/src/tools/vm/start.ts +++ b/apps/mesh/src/tools/vm/start.ts @@ -62,7 +62,7 @@ export const VM_START = defineTool({ vmId: z.string(), branch: z.string(), isNewVm: z.boolean(), - runnerKind: z.enum(["docker", "freestyle", "kubernetes"]), + runnerKind: z.enum(["docker", "freestyle", "agent-sandbox"]), }), handler: async (input, ctx) => { diff --git a/apps/mesh/src/tools/vm/stop.test.ts b/apps/mesh/src/tools/vm/stop.test.ts index 1543083845..36b5284ca4 100644 --- a/apps/mesh/src/tools/vm/stop.test.ts +++ b/apps/mesh/src/tools/vm/stop.test.ts @@ -201,11 +201,11 @@ describe("VM_DELETE", () => { }); // Regression guard for the invariant called out in stop.ts:1–5: a pod that - // flipped MESH_SANDBOX_RUNNER between start and stop must still tear down + // flipped STUDIO_SANDBOX_RUNNER between start and stop must still tear down // the runner that the entry was created against. - it("dispatches on the entry's runnerKind even when MESH_SANDBOX_RUNNER env disagrees", async () => { - const original = process.env.MESH_SANDBOX_RUNNER; - process.env.MESH_SANDBOX_RUNNER = "freestyle"; + it("dispatches on the entry's runnerKind even when STUDIO_SANDBOX_RUNNER env disagrees", async () => { + const original = process.env.STUDIO_SANDBOX_RUNNER; + process.env.STUDIO_SANDBOX_RUNNER = "freestyle"; try { const metadata: Metadata = { vmMap: { "user-1": { [BRANCH]: DOCKER_ENTRY } }, @@ -218,8 +218,8 @@ describe("VM_DELETE", () => { expect(mockDelete).toHaveBeenCalledWith(DOCKER_ENTRY.vmId); expect(lastRequestedKind.value).toBe("docker"); } finally { - if (original === undefined) delete process.env.MESH_SANDBOX_RUNNER; - else process.env.MESH_SANDBOX_RUNNER = original; + if (original === undefined) delete process.env.STUDIO_SANDBOX_RUNNER; + else process.env.STUDIO_SANDBOX_RUNNER = original; } }); diff --git a/apps/mesh/src/tools/vm/stop.ts b/apps/mesh/src/tools/vm/stop.ts index 1175384e87..0640d665af 100644 --- a/apps/mesh/src/tools/vm/stop.ts +++ b/apps/mesh/src/tools/vm/stop.ts @@ -1,6 +1,6 @@ /** * VM_DELETE. Dispatches on the entry's persisted `runnerKind` (not env), - * so a pod that flipped MESH_SANDBOX_RUNNER between start and stop still + * so a pod that flipped STUDIO_SANDBOX_RUNNER between start and stop still * tears down the right kind of VM. */ diff --git a/apps/mesh/src/web/components/vm/env/env.tsx b/apps/mesh/src/web/components/vm/env/env.tsx index 5f31086da8..136191f648 100644 --- a/apps/mesh/src/web/components/vm/env/env.tsx +++ b/apps/mesh/src/web/components/vm/env/env.tsx @@ -63,7 +63,7 @@ interface VmData { vmId: string; branch: string; isNewVm: boolean; - runnerKind?: "docker" | "freestyle" | "kubernetes"; + runnerKind?: "docker" | "freestyle" | "agent-sandbox"; } type ViewStatus = diff --git a/apps/mesh/src/web/components/vm/hooks/use-vm-start.ts b/apps/mesh/src/web/components/vm/hooks/use-vm-start.ts index b3f56b392c..45b91a2825 100644 --- a/apps/mesh/src/web/components/vm/hooks/use-vm-start.ts +++ b/apps/mesh/src/web/components/vm/hooks/use-vm-start.ts @@ -34,7 +34,7 @@ export interface VmStartResult { vmId: string; branch: string; isNewVm: boolean; - runnerKind?: "docker" | "freestyle" | "kubernetes"; + runnerKind?: "docker" | "freestyle" | "agent-sandbox"; } const inflightStarts = new Map>(); diff --git a/deploy/helm/Chart.lock b/deploy/helm/Chart.lock index 63e866992b..327f1855cb 100644 --- a/deploy/helm/Chart.lock +++ b/deploy/helm/Chart.lock @@ -8,5 +8,5 @@ dependencies: - name: agent-sandbox repository: file://./charts/agent-sandbox version: 0.1.0 -digest: sha256:99163f1364ae0124dd8a4f7a1b93430fa43f7bbf13683651e2b0f8eb019ad844 -generated: "2026-04-24T17:10:45.814632-03:00" +digest: sha256:496ac6feb3655dbb973acf67117b2d3f03568d51178ef6048c86c93813b63436 +generated: "2026-04-28T17:44:22.721996-03:00" diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml index 67a0fa30b8..4d71400474 100644 --- a/deploy/helm/Chart.yaml +++ b/deploy/helm/Chart.yaml @@ -21,4 +21,4 @@ dependencies: - name: agent-sandbox version: "0.1.0" repository: "file://./charts/agent-sandbox" - condition: sandbox.kubernetes.enabled + condition: sandbox.agentSandbox.enabled diff --git a/deploy/helm/README.md b/deploy/helm/README.md index 1e48ee17b7..ddc44b3d3a 100644 --- a/deploy/helm/README.md +++ b/deploy/helm/README.md @@ -1339,10 +1339,10 @@ kubectl top pods -l app.kubernetes.io/instance=deco-studio -n deco-studio - **Liveness**: Kills and recreates pods with problems - **Readiness**: Removes pods from Service when not ready -## K8s sandbox runner +## Agent-sandbox runner -Mesh ships with three sandbox runners (Docker, Freestyle, Kubernetes) for -isolating user code execution. The Kubernetes runner uses +Mesh ships with three sandbox runners (Docker, Freestyle, agent-sandbox) for +isolating user code execution. The agent-sandbox runner uses [`kubernetes-sigs/agent-sandbox`](https://github.com/kubernetes-sigs/agent-sandbox) as its control loop. For self-hosters on Kubernetes it's the most scalable option; for single-node or dev setups the Docker runner is simpler and the @@ -1352,11 +1352,11 @@ default. ```bash helm install deco-studio deploy/helm/ \ - --set sandbox.kubernetes.enabled=true \ + --set sandbox.agentSandbox.enabled=true \ --namespace deco-studio --create-namespace ``` -Then set `MESH_SANDBOX_RUNNER=kubernetes` in the mesh server environment +Then set `STUDIO_SANDBOX_RUNNER=agent-sandbox` in the mesh server environment (`configMap.meshConfig` or `env:` in values). With `enabled=true` the chart installs, in order: @@ -1366,11 +1366,11 @@ installs, in order: namespace. - A `SandboxTemplate` named `studio-sandbox` — the shared pod template every `SandboxClaim` references. Image, pull policy, and resources come from - `sandbox.kubernetes.*`. + `sandbox.agentSandbox.*`. - A `NetworkPolicy` that scopes sandbox-pod ingress/egress (see [Security](#security)). - Optionally a `SandboxWarmPool` (disabled unless - `sandbox.kubernetes.warmPool.enabled=true`). + `sandbox.agentSandbox.warmPool.enabled=true`). With `enabled=false` (default) none of the above renders — `helm template` emits zero sandbox-related resources. @@ -1382,19 +1382,19 @@ emits zero sandbox-related resources. there's no out-of-chart install step. - Cluster capacity for sandbox pods. Defaults request `500m` CPU / `1Gi` memory per sandbox and cap at `2` CPU / `4Gi` / `10Gi` ephemeral. Tune - via `sandbox.kubernetes.resources.*`. + via `sandbox.agentSandbox.resources.*`. - For preview URLs (`*.preview.`), see [Sandbox preview ingress](#sandbox-preview-ingress) below — this is the standard path and uses the Gateway API + cert-manager. - The legacy - `sandbox.kubernetes.networkPolicy.previewGatewayNamespace` knob is only + `sandbox.agentSandbox.networkPolicy.previewGatewayNamespace` knob is only needed for setups that route preview traffic *around* mesh, terminating directly on the sandbox's port 3000. The standard path lands on port 9000 via mesh, where the daemon's CSP/HMR rewrites apply. ### Sandbox preview ingress -When `sandbox.kubernetes.previewGateway.enabled=true`, the chart renders +When `sandbox.agentSandbox.previewGateway.enabled=true`, the chart renders an Istio Gateway + HTTPRoute + cert-manager Certificate that send `*.preview.` traffic to the mesh Service. Mesh recognises the Host header and reverse-proxies to the matching sandbox's daemon at @@ -1404,7 +1404,7 @@ Required values: ```yaml sandbox: - kubernetes: + agentSandbox: enabled: true previewUrlPattern: "https://{handle}.preview.example.com" previewGateway: @@ -1490,8 +1490,8 @@ where the image is loaded via `kind load docker-image`, override: ```bash helm install deco-studio deploy/helm/ \ - --set sandbox.kubernetes.enabled=true \ - --set sandbox.kubernetes.image.pullPolicy=Never \ + --set sandbox.agentSandbox.enabled=true \ + --set sandbox.agentSandbox.image.pullPolicy=Never \ --kube-context kind- \ --namespace deco-studio --create-namespace ``` @@ -1524,7 +1524,7 @@ the full story. ### Security -The default `NetworkPolicy` (`sandbox.kubernetes.networkPolicy.enabled=true`): +The default `NetworkPolicy` (`sandbox.agentSandbox.networkPolicy.enabled=true`): - **Ingress**: allows mesh server pods (chart's own selector labels) to reach port 9000 (daemon) on sandbox pods; optionally allows the diff --git a/deploy/helm/charts/agent-sandbox-0.1.0.tgz b/deploy/helm/charts/agent-sandbox-0.1.0.tgz index 2a77c298e9856dfd54b15292da7a26603306d940..7a9c70188d60100f9d7848a8ebc55071aba76cd9 100644 GIT binary patch literal 24096 zcmeJFV{{}@121~Vb~3ST+qTV#ZQHhOXJSq=vF%K3n;rAb^W5{^d(K_w!@ZyH)Q9e> zwX17&ud3?*#;-O(G!z=pf0i#QAR0qSWkzF3Id)l3E;b_;bw(2vHcM?4E_MZVb#^%o z8!JOQ6Hiqodp=218(W}@u5+*b;RedHZJ#K9?CV^t^%BzUB-!7|#gZ)3K6$qmQY_Oy z*Kfz01Bh2;fUr;P6pK*v8MZyc-gy<$DYyuLf-n-nDzSm=&t4N_hQ#%&&X-(wK_g@} z0HZyLPb0@7wX*h;13SeKy9eLPIYZCn=hyej?pMCg=V3ZQuAl4cIfH_{yn;f{>&d}= zw0^$d2QrVP6UsT^Tv%1SyjA>zgQGVHFSIncLe*e@z@4oxOck5QjTY8ZE zZNb(Flo|xIy=q91fWZJUnTgV_#3;WJPzC#eg(Gwz1I~XgGbB7aAU`PLF^$fDp5d7D zNf!sYd+U-jZNVP8uq9=_?(H&K@9`zUD=Ek!2UL0WrBR;z_VseA(Zm1pUYxwZpwRO< zzWF8Bi~2ZlpG1wNzH#IK1<1B(k2>;~(NMPs)#1$bf4++#`RxP|eFXqh(Yf<1{NYm6UNoW&F4;W0^`ZtH8W z6FIbJ&8V<+270&*;=>zct4K&+$>Qgw*Mae%EU4aL?s~zFr zaDWet*b!Muo`oywR>gn5I90{X($S^$ElZMY;xPB*CRbZhYQOjcej@s?@DLqwxC!ROs1qCX1u;{i!@D`+!|gAx zKzLcXlH$@oZ_W)75C7+mX#SLZM&XTR2~kwn#-h%dhWP8{0*V{9=-iS5Y{1v<8Gv9+ zGdNj`Zw+b~E{DKwJ3P0nK<lWOI(KS9cPvx{UqOS_OCWX-Z6T-?AaAE!y*$IC_uI)Hdj*6Hv@OVwJG+$1`1QEq zS$&L@el7QD>{JTtd~%=jt;v8C zq;r)Fk>*KNJI8R8rPqs63VFKxo zZtcHY(Rwa_QGlPSp$Lkzz^<|W`6VHNbtRwD{BG~ajb}^9OoBy9z+euhI)|@^w!XFeU(L(D;QIGX0eZt+3jYw8FsOHu2fU+Giuy?!KKnJ5{T^bhS+Z?f_Wv9@(IEN9n?}K^idY|=q==OMww?{ zbUT`@=b~y9m9;y%LsTb%{hH0K8dhfEki1}wMh982{6V>$cnDr z5NdiTW^m@3)RLQJP@8&<+W8lk$754R5-ajOc6;JY4iXEMx$ zn|H|{kD)$h#hV&&=OPmDyPz}p9dCcVetmw_`G3CYf0;D#AANEjWA-ogqLLGXDvh&y zAjM#XG+>$2#5bDz`k@BoT|2!}wxQ=Sf^YI;+H(T9N8RMaZSL6n?2;Js&KFg>m6-MTJgIx+Z?U+Pn z(LpThfUtp4PUv3Vn4g;fw>kLK}35B^z;GSaN%Ap%}oHCn(rvej93YW`@vt*bPiiOm14C)8s>W^ z1xBJMrr&NHhE%pZ3KwcdoV&uyeH_vUNe6)p2vhb*Fnru_ae~B5rn2>u=HrC*Ot~=- zE@ajPkEg)Scnm|jOPLh~oyC&sQ0leE+JLoH9$T1TcXAV)@w(9aoSfi8G)n$i_De%= z?iwIp&O>6~5HTN`VS|%@6tbsPw=hNiVE~~EtaAFb5%32z!fcsNg#p&7%sY3QXUu4K zvyn89O?%OJJHTit_zicScV=`3etAHC9F`Z=q}Jbf>5@fYpmU!HY}Zlz6aOZB|3Y$E z_h>p9&6YULk8db(QcWV1H$%6$LNG`nGiYy{cQ&ass%_d~K!bT{q1XVGp$IUEo@l&F zkjyCWpJmq@f;pA8#FLlUYIEMBDYGVMo_ui6_?L3w^v0U#yu zwRsg^KYv(Rz}g=Ddgp=Y5@L8NmKESX_1)s}T*5j!!4)|WIf$hw16?}Dd-56?x@YEy z4f=5mfG%;*zCL(C>Ri9bqkjn&WA2)!a;oi(zF&&)8!i;!WnJ8I0qX9hGy73${vPy2YhoBKt-80L@b`-vHa?$^8f zz|W_126=w>*KKkGiT5vQ(`(s>GroG=Xf7d-v6R-bR`6i_cw$SZ>(-47E^|U0!4xjh zO;X|;A=6xiO#^wa@}A3F*&3UL($91_B==%A63<5`X9r_U);! zhZaiQt{~CdotJ(QEMqT9HM97VFkd5dgf4L_MF4Mnb>xxH7x} zo{SCx#Xt8SjMWtQLnU7w)K@XOdpgPPg{W>AlJ0kuv3DqbS-e>xjh*J}Zv>}jl=Aez zgap<*sjlH3o5Liw?qPGVfY9Vo>u-#}G}HIAP6Yh)>Y_Swfc?Q)52NVr!iFZk7K@Oz zPJ1e%og<3CcBJhynYw3u*ZQ={)j*eu)1JH)0~acBM!zT-3X=c((=JMBkO3x#&^OFL z?p&bQ&Q4@KODQxRB#1ez0WOY^m9K&XDvprW=l72w1E&2@M-lvm@Gej$OzaD+X{kDW zA9-!{KeRlEXq{)%WA+tCFHXpEQsF{ES=trK7jh~_Y_SX_S^W=#;MaJRw5KFWQWSau zU?Z|Ly-X_O81W&Q+^1eYID-P-SCB)TRs@3Tf1GBuZ)MR8k;u7gWr#@s8O3Yf<~d+& zxF={>^!ojLf3b#E+XR_=9@?Az2w}`t>S;E^ev!%9WO`6dUspLWi-Wn5*3iT~nY)

z?y;R20wn-BN(IDvC{u7?fp*3w*?6bQ2egOUPjR&)} zCPM4#(rr<-a1&pxm-S?BjOfSC0TMAMmkB}+t}=o=Lgt%G&p`qetmC`u&<=B2AZ|=g zu_I!1sWO$UyPut{*6ZUZyw4nV$m=j)2!o(?8aE9f(;Lhm>T0SAWFs5nFk0g7$dt}( z34_TxQmBM)YkVLtcPz}KgYGdQ&IX<^s|EU8@M&O8_we~s#KETr1b4gKXZP8Ui?drF z*o9?2@QxEU8QIa26$ffcwmDiHK4@FTBn05=)U7bFvxUpS*5>W(Vjp*)u$jGI?B(p@ z$M<8$C%>}qh2OAmj7ZeMX%v@}s{}U(m-*?`Z;WXA>6eYe@!Nq#_YN?)+YG=XPk7riy|*8^M{nQ9!2J8d zUla7(HyW}IZvuY8KdVps2h%J{U0@&a{ zzH4mqqY36+_>fMTp@IhFTt#6jG7I1iNvkJFGJ-j?wkuDcI?`pQcw#Fr=GEvXY-GF7 zLm3$l(#?opYp^v1{YFAB0MR!O?1+npduy+cVpI6aV_Z-LaD=C(CbqN6*Y zcN=J_wyHx7JgqH}?mQ`b7ec(LFZT5P5m*F-lXl!pg0t*z%?xNP708W4o@&;BW+xVy znGI<$X~^{$n!oW=sIW}OYBzG$jC#*oYeU7zhm(klcV(On6c;97NYbKGEKy>=HKfbn z54Vg9rsn5TKPjfOv7p$@sz2YyMETbV|J%Sqxj>$a|Jy>j!q@!7=j~XN0PmZ`EkQoN z_v8I({C3yN{$P{;J0{t#?Wg{i$16$aGxX|MlezzM2I~~K?vKP%>BT9Hw;Zfg z#S{I&MjmPHsME{O&E*{`#_*x&+%zUg$V$_MFtXCLk$Ri{6IWS|)9DT3JX58npXOLF zp1P*!{hHj`Oz?73gcfI_<+7|+h9Xq76=1o-v%f%Jj+VMw8iKmp?1u=`o0!7M+JxLN zW!Ocv=A2Q82vqTTUr!ph=#vTCQUY(NONm^10#AZD9)rd+m5~I})^u@d6%m&H;($M$ zSEemrXYb})9qlR&!X?xJW;3331bJ51a~2h?T)rmVmKR*#q1*Idq1xea9-512=&OP2 z%HiWI&KW~c#xVrO(##w)pC~`pFRA!20=KvcU zHcB)nRL^8(yIk-tLkcl}LwmL;WGPaq70KDmUn2^;{RiH5g(Rp_@ws-g%h9%c+o!KL z!@Md|+>B~pEeH_X4sfhu+hqB0q0LEP3Ot1i(mOVyD>T9(7iFMUc>3O-XM(w+W zoLcj#DQxHJvkTxz^G<*WtE0y2!P*qc_VDnL-i0_6GQzYmG_2-DX{e0o(o1fJ(Xl3*@Ie<*`atQoQbQt?#AbI23r%U*f4RX% z3Y7HV`_qE}kI#awSi;yD6U3h!`m-*n0J};%@un@DhGy(U7w#&QE1iGsUH%Ag-!6&X znWqgIiMwh6Na>{&vAO6!Te{VO=Zq#-%OF~0Cr0(F=1$m-=|a6=^N2(-^PwjJeM)7f zK3uvbOmI(^ju8a{^(f@Scc6V3J&vDh8s2}F4OP~oUd33H>a2j3B1*tVWHyI?$D9lc zF(J4J>f^ZVw3Hg(XX-%??7iZ7NNM|O%Rj>c{Ge9>Ig# zi!~7t7)5uKiQy4k-Qxp-c%Vl>Hov=#-0pK9NqqCvmh=S3<2=d%^9ACm0WOgPK{Z*4K|J%5`8pCBxLU+4F|Y3w~7Z(XhaLv?JI8H(#FuS^VJxCOo%iTcul+HYWvg+cEgUqc+SGS`+^FE67W-?-4_;O(RIAm=lpF^?K~Tn|jz{Xt)mTEa~EoY;CMjI=P85#cluN zR{JjN_nGC1PMn^_Ovit7Yb=v9UZcVi(p*b$)M~H6I*=~q)V(}hG!Ka$O&s_slm%G+`@bJx|D6ipoTvt4 zxH-@_M4YGvEWq5T2H+Rzl~=L>Ro{Dj-}m3K5cE+*^PO1JU@KwJih<|?B+=o`Mx8YC zk2?V6L@jmos2~5^F!()pzw`et3X(vcd=t%4|BxCH3eDFive1^q;LU_D2SADTpO1aG zTHIvJ1rUesnxfrj(%yTeg6ULt4R%m#bt{$*~52z>DFH+7E9*KVo-^LbLFvCkDPrW2HKsdmxQ= z$dHK>2LN}@;v>$Mp546|1FCO1`Be&4)1@E9Tu~U#gq5L6MuI1`Km{P0h=^7pI}C2* zcMEPb_Y z1oL<(XlBX?;xew%P9=Rs;cP-Hl-DD6+$^bg!;3X6pFtIR3CrS6e0P{@cUy)aJT>#< z840T|_#898I?_jZLPRMldKNFo=>Q;D432~?W)4v88Y*oz6dI@Hle)+;Vc`^PSl}9~ z1vw_w0iTv~D|9|4J^xQ4&_W=rWKMmTKzt{`cO`*hm28Efz40UZI>%()A!%_KBUK^6 zLq}w3bG<^lu-DzP4Lg9V9L-ZqKanfP3sf5bYyji5eA=zWfoYuJYsk)B$K(V2i@y!e z9a{hgM;|p;5=IB+sJu;VQa!?=GrxsoPNvyabn2GXTV?|xE9`W(#?W%BBZ-Ni;6udi zka`l|m@0Qab_J$W)RHNPCfe|3ksvvHN#1?_Df$ibrELkDD$D4$Pnq}v#uj?+d(em& zTQ_37SQMZ5=$RT3)DCdX+Y1IVaVc!r_Z*ARG(^9y$#QPvym}NsJ_;kg*56Z2R7aVFx4x*ch4V>7%+!Quvse8G8<+Y?kemJD z^o$VORxH~_?gvR{_}cxuvQ$cP%~VGNMR7M~=kBdLqqxRyD-Lz2jv#dsxMsKFLVmw3 z_1AJnK@7k^E_<_}t%zyD{9dPbL{FNk^WPj=8L#~z46Q>@{TBELclbEVoMOE3x?yi-UZrW@^O_^71f2?bgI(z;hYQI#< zW5(!1!SH25$wE1uvZ-8I{AH|BoU}PjVMbZdaF)lCvZ*w$ENlOC-92q1_&+y$N;_e$ zj%5gpmVU?kAjLAK#r>}IKQKdLb+zKz%!V(mFF&B}Kiizc- z?SpR|&}<|ckMd_kcq!m=pEzC68L&(EZ}X;-q?IayK*qKfe#FyX#U73Q@o$;Ec)XA) z`>)pOLjkWFWNUJuu|00oBH&f}<@(kL1QRz&!*X0EnxW8D5~8MP%xX4bJuJif6CpEX zp-~&Q%$lEO?QF|u2b9Qylfw>X_UujUS!`y}MPfM29V`V~I4(daO?}>YKn2NBtBNAA z@kG=C*z2kR@OyI##*GEP5kk;(N8K`Ic$Aya-6A0pvPNKP;pt%u7lof!ORj zFSs1E2it{kt@RP*qbVWF#W?Bu6SGFD#Vknnt(gS7-4cyykeZ+dp>!KwsWf+(^hO;$ zIrZ?TQ+MthrU&H7;GF-nDW4vW3J#gWFmt%83r~-DtZsgA=Rn`*HY#^1Lr^JB}QTW*<_CJ`B1x=hjPlEggGS8avG1^46h zshKd8nZ|&G#X;j`5FN^eYk9;-ULsw}7?3MibC8@M7T}|8PShP32lCz|uJ08Q2XaAk z5Et@Zm=y-K%}gMTU>0CISMl#=#Q(pp2we}4S^z@lC$1i0Ipm>Ha@luWO59XkoyK4n ziB6&jCjy8bV#}BF)wqzfXsFY)A;VTg8isA_J*5RZEW7-RmgYAg=(tWAFQj4&MIDnW zL9G6xq18G`-O#$mBy+}yvQl4Id8eD<&W1Q$6pr-&Ni)Jg2K=#YSzC;$4dY1n)dIki zIa7Z5r_h9{W#=KYCEh@VOoDxlmyB@&38$mJ-Su4^bh=3Dhgp=f?azY33ecCS6`sDI z{~e$`W@dKKrz%#q!$Sn|bpJId3C0+Jv9=G~j&I2CZW$IxU+ejVN;fa)CEpf>-*0$SAJvj!|(WfD0(QrY-}|x18iuV~~^Rk^S-j8{|e>Px;s9L4}A|ZkQaN zvKBK3E>G1FaK#UG5?gR(!rzi{g7AUqF+zNu(yrKb!o`b6xh{9DOWVjJ=rjS0-c5D$ z-bg%{G(@SCDjp8f3`d&L4bz~tuC{k9Q$zQ39HyFDRMkd9stV#SwjSXQFqSDCbTOTH z$*ViAX)VyY&_knBqqRk8lV~oh%l(rc~ zu*Qp$jVQq}yJYv#YO#vr2-=H%)ZD+V5R87it-+IHFOvhIKEw!lcN6mIT$I2KpLG`# z-Xde@oyWP>#r>Fr!lH;qHccLlK*~ytZs7nEfUe9DzvYXxsHZ~i5Q?0Z5|ar)NFBt( zpt3+__?q>7gGPfto>4%$WZFaQ_a+##dd;vPf3M<7bCeC^Be;WlT8Zi^9(}Gp(g=wk z?&mKfB&pBRA1B?ayd5YUbVRn}pm&Qd3M$xZ`7J?G@2KVR1cqBvAZo~UwlNw<;fN~; zmAES;zL>>`tLRg36BW<}5qnScb1Rx$YC+SRoD-@EZVlW|M;t51`VB&sLvpYfjBQV0 zn>-=W!RU8ibO@0oa5Nesicx}5lMTY01qy@fk>}wbI7XOTVU33eYexU1wUYE3*`D|K z=<6yk8UZA}0~m6NoN2j{&dDm&MN_B!iG=i*Y~m%$Q^=ToAIv;+6QC#!q>$kl%-QQ; z4ND7&?O+{?>82mqLU8_4(DO8GNm ziR^mq2`e5?3SF~=M`P2ujsurgR6%RZCdD>>VQGfgFx zl9?OduEo|$)e%~^z_aB4i&C>P*bEWv?ia8dG~g%(?(K% zJ|02A+kqhFk{Fqa=2+NwOL97()x$t+I1E36P^#B6f$6p&B!$|y(*ZHm*FhwDO)?cb z>)U5vS>qYo-6f}3-#oQZDYH&jW5NT1f24%iL^>XHavk7VF+)x&?N_c3ri-)t!IZFd z*^*c}7b55zdpl&Jnuq3W3dSy7vB_v7eSkP*rDD)m6b1bCT=41z7R&mLrq z_ZC5h-;(U{WCetF51Bm@yT5;)sqKI4)vdFhBW>XL9I!iKw}PZsMV}Bv_E;<|K6n<~ zTD~Mas@{WB-#{+jd@NRpGpxxB?h_mqwMd+>dcaUuw;;ZW6U&)7*%q4fB21!dY&K`u zsCeuM@m_Ge4x*1sVn)h@i!qvOQkCG$k2g@IX_J_-r$alxj3rNZbc3k!qgPJFCoNPz zqhGpyN#GhZhdT>i{N1xIp^U!Hq=_C*?gvylC$$Mv?_{4z3!6Vy zy=1{2IZ+NE88<7jIdDBSMu=B!&OI#m_)|!;H#X{kJO?v&VBjg^^Ig;10~)DV&2X-t z80&nd^K|6F#p;2IEh(QV6kNqC^%}Z&%RNzATmw~9pJNGTWH%Pwl9-Q1c_t=GNgM1| zi^$(?fdZ$_N64z4i^R$dxn$j~l&ZanKu073Q?qR^Fwm~epd)>;5-CUMbI8YuBa1WP zE{9fowJAZ)Q)~AKW=P2gUG?77c4@dZupbQP3%$3f8;eLP(>Oy!s-v~3fnC333u4wU<=3chHcR;~YUr**?831tTgGE@v%`W}Q5*x+=1~OK* zMj6L_!v2PAW!a!(9H#hIL_Kov&cVSd`$flncPUO4T=qIs&JggY1>h@^rd&Ilj_5ta zL)tUJY|d3pr|@UIr+e>HnrM9oPIq(}pwi<-e z#+6{dv5y?ZPtNzh_zl}IM$KYZH&4v^n#P*slRpL<=e(&@%{WLeki7uLzrWWg_iE>TKu-6U zf*g_DNX>OLp*yT~D62ctv5M?stD<|amCFc#4Et(z;{{#IM`Xn=V9q&JEF zlaADA?9Mz=mOzo073`NGCc;WBW?0qKq$dRW?kTstv|fBR%;;A|9$+8R7 z!sXohxq$sJR2#LpW~T3Tz^qfrw&;zPBPyx+de=bH&>o^$lzY-oOB;?oeRG&x0Gc}< zvPne3<@YkyN`o(KoaxZP85CI>h!owL$b=~yXh*d=O9jS}cuxVKSlc$0&b@W_V$eHX zM2=qg_#oENksG5B=gD~m^vc*ZM()O0IBX0Hm!7GtJkZ8WSDl3I0M5!u*;Z8;bgL!Q*C&v=JBR#9wf(zj6lkD>8JbbNFN| zqlLRE5L_!{y*YjAr<`1TM{sTtG3!+b@sjZM8SbLdIN`B3*qRbEc-Gv0_FPFs)L~!xn_7vSrWCp1Z+ZWLY0o0_aY{P;Of;AJG(sx z3~zECIIXN;d^0a&izww%j6c$ls3p5SPGJ#vdlZEZ7fVvC+0;shfg{C6O`uo5p0&|{ z&+hQNd^O$7+GXWFG4Lkyn#*el3fY&0i0`byP*l^V#F>e&AQKnK!5N3Wvo0qt{{? zAyxcvVkuuPjb?FGKS20oV;HWBT%{DsRl0&)RMP#zE!3!9={o{|CYkYFmLF7t{D#oB zBh{X&R~YkgPOuw5o}}A|p-@G3q?O)@3Rxe*CKSQ_)fY2ZKZJ*%SdA4G@nj>6e(E;T zIWiJs6-E^mONRHiBxLlMJZlR5V^Wm0dtMn5g{Qm(oOB3~ke^(kgx12cXRKf6z@QN_ z=mTcW?Wba814&evho@z?6}DAbFGJNHqn)LuWGXzP7XJ}ai=Cl1w3?#V*&-Gz^QP-A zr5>N6MFTsa2+0(|zCnsu@55()W0InuNNy_+ynK|qmpJY9)0SnTaLtLmXDP`UPUsxv z=_+BnC(ObiY-SZB|LgU9dj)~SzH##NvlU4QUstuIvGKVO#ryQ#XZ(O4T(>TNaHCN{ zfuA9BxOtI#e@<2W{kHne-{a{xKTrPCasJ~{U%>lwfx`LgWx;E%)+t!S@ET5C$IJPo zvDlqD&{bhv&+R@5vi;!cY0j4Pda0{eV|pWn*L1pg`*b7XplJ)NUP5&57s7 zfH!*IRFyL>F-Ky`sV}lWT8+@ApI1#v=#5L}40qM(`}%b&!8IdaSX1=RZbx ztTSA1>Mgg|ym;;lMqz0>o`Sge7$MLip5R}Z!?|oxF#d{afYHgE)b)L97uq@VI>`d>o&ABY zy0zHTk=YO=64qwN_0hsP8UvcA3ki)SSA*!$!g#t%ZhTT#s;R&HkMQd>KQB6$IAF&F zN_^f7J--`~U(D&vQeT^)?4GE|@JRtx$B6#(d9qBlXKDNHnAtY6OFHh26vZm|gCcoN zs+aIYIsE-&|C0uDOQZ7qE;+R51>`{1zFC!9DO9oLYE zatk-!K3@!ITppU5Dn5{i5Qmq(sSpiJwWjh;uXyKVdC>IJqtB`Kls-8UVW;3js=@tl z@1gn+BPxMf>M!W9n?i%q`3G(Ns22i#X>ckneLzCf(q)iZzWJXwa1oj$s9T#uDg{o# z!mv6YRa$}K2aQ4(dDCt|nz=R;WOv=rs$vtGs*2j&eeUd`yRbsd7#6+i=haO5oYVgQ zyabW^$Wq>fJ^%Gmfv)n36p@MW2Q{AvSQFwFbIcY*KtWcOdS^$UT0s+GR4Pj^ydSI^9a|6guU$W%{87hVQVZ~tQw zwLug`{ODIIe~#K?=rw~qw67w}do(Fuo3%ZZA4-J9&Hsr6Q#`w7fava&7bAVjQeZ0B zVWM;KQ-edB?e&4zD?eLGo%(27s@H<&*=ljCD-)Z%oLd(AoGhyOn{97xy-sZ#?b1NC zM6$nZXke~Fs5{Y+W?_hUf@bdv;s`l|>Tg!np@N2D&M|U(zS$o!0q-TX!kmi>_D|R2 z0}fSy&!31gbaWcr`M<)cM)p}KRhB+Pi)c~lVeSR>C3O$SKooj)z$3DW1UzQi2yY{@ zxh`EWxI%({7f`}n7X(5pQ7&Br@;);CxXR<3sqU+qukkBZd^$c}ZniMXuw&>{e@D}3 zmJ*LL`PS>r_a@U4o7y`o5k%|_9CU`K&lug+{kI-6c&*dsSgg%vsxiNf-|J-FZIgEP z){a~9M~Ru@h_MyK|H}M2=A~;VZYVo(v0y{pyj6kGfr(ihoS-0baE*ZF5vY2{h~yD0 zPn*pCSLR1k)mN9rV)^U7BJR(h7Sq%9H;x-IYos%rC!}tm8m+VHN%Je@ccM0qIv5B0 zpD7IZW7+8~8Cv>-)npKfj~+CL-frYLdzZggM7SH-0xXuut3fvrt({Z%-w^w5KVTeu z^6x_T-wt=4{lI6ZBq7K5X=TL+OXq9|Sy|@jvDu*f6cUlApT=JJ>4LqTH~#KiP0e+2 zB?-9N$Hkvb&AmLR1^|33A&%cdz$wC!zOA40&Ow6HxA)s9e9=UgvPfAd2y+--Ls0nm{r z_pEE4?Q$c<|M2+D^gxgK<9nW)|d_@Q^*0{wj>r%{8ICsiAh_8 zVCs%DS-UYfR4qj6GK6Ogp9`jc_zh2q3ju_g8@x@ZR#<0*5~)@W=||kvup|^1BYm9`Ppb#LNJql zOh}nFT4E^gaiI2J=fzfZ^1cilP-?H~&+}7#{tt<#Rf0VJkK2;B^?ZKc*Yux{@d|k# zyQ>0GU(ffeP2~#y?^jc;*Yx`Dq)5ExFI}odew5v#_Vlz@9K8l_{}I1`#P1*R`$zo#5x;-L?;r8|NBsT~zkkH@_ z#P1*R`$zo#5x;-L?;r8|NBsT~zkkHb zsI%+gU-!kNcaPthyf|O&qpop(r5~96dOkK0zqsykY2UOnr^k=H)$HvGq?~x1>-dPh zg7)O)zBExY{U4g@Lr+jZCW)$e+ULXmbGCtzPmU}YNYx!kiJ|M5BNEC-QR{eUwm5m~ z)ZUvScg)o!OX4%OvuB?=rWDS_-paq&5nOSUJ0Mq|G|`uj>6Ogka<|uJ`MT$ z-x*Ckr`uCNd|S_!mxYhL2_Pf)`tDc(dIZv`V;9MR?Xug4Ph!y)31GT(zAJ{Z-pqVE zzXtdIqpB2^6x{osnAH#$J;%+9PAR`?Ot1b-dc6=mDF=G+77`BGjz2w7f0VMsYXM-6 zh4qJSA9=x7$GTC8ECvQV3P+lL`SM-QFxW|c0!~sOZxiy5-gdre)>3pjlc;%mo!|4G z*Y3>u$x}O2I~$tAAJV(5u&Rlbx{hTTveRW295${1(;`(nLMSoPeo~)l`K5%KMfMH1%IVCbj20Dh0Y?=e>awXRAV4 zBa+z6=IJ|vE~u-`k~ha|WjGQi6aMZ!(0_Kufd9Uzgljk+h5gYnp}6umobofeWwmK3 zZzK(?W#kw&nyv&$hZHb0UuL`Tf_AL@A2mm}E4|e>PzDarwBV)S@M&I8aQwJB0KttX zyemCBH7qvH`CfcJ4%1h^@^f-{-@obN**;4F%q91`H{<z2{b*pl zY>c&Rn7JsZW0Q&}_i9czYv>v6z>%%t%0 z`rI^)3PGZ1%z`!-AXGr~F(um;z;C8~R9u?Ok(H~jXW>RMnGk8y@IBY$$v7pZdtSZZ zeIJfDaD6#;fL<48hX=#%kC|lZJ@G8L`>Z&Yn&`_lLcU zDSdwrN7s+D%cpHz-}k45M`JY}Nj~|OK(F6*9uZJhA$Zz>y$wTg+O~unMi$f3k`rTe1 z#CV=dGYliEj|^~s<-SsK@0E@>vnQ5m1hR7{_gZ1wod2G_zg#Z5pPCG;-@`7+9UGy= zuhyxKwpZp)^Vk9==IU0Wzo+@sD@PseTQI-4rj>UM<>z^w7{kzi_jv7K=!Wy3L_8|L zV4xs+BY0()L*{vXPA=YV&~HRsRU(K1V*f9O+XFb~2NJ;ZP&ypW~nSJ6HGW=P~rYd#mh8 zr*fa1*D~?gu)i_?Akqtx=e{{@*B&U9FGQj&#K6Vp51yh-ET;i-4R(+ltbC+6LD7UL zWF;`ax@4u)o-$UFp=bo-??+a9oA0{0UZ3wNNeKN@BVUeA_GjWVSVa_*zuxyK7cZ{u zwA@o#q!ZgLJ|>i2m#bZ0k5A2ea2xbZlSBj}nZxmy5K;JivQjHKH9pN{eTGep?tYaJ zzFt@kcE5z>@*2a)7Dbt7R`b1?RYH_j`BSfOT->|M)_*;~NBfNomgi4VFeyKm%YC6BqBzWei!+41cu`#JHnF#t=MfT^i z(lOWD$A#Op(m-gB`5V)VVOWv%7_`nJNp~w5cHk9V*MoRu2G)>Rg5xqH`~C&Hb%bZy zFBa1}f-o%qT%UH(DC^cF%O@Aa9E!pLOc;M2DQzy0SQ;%k^hx0rwlO1q3vmem~%1zUl=7@g?9A_Qx6W zZbHd#?kKKl-?0g0*RSWVlibK@_Yb_8-F~AH0O#{tX&KjLo60=U+w*DeNIUSGKH@CB z@y{$*{~2`imZJsF>YOhZzm*FWeeqdi4J2U?P-?!+jlZlEo@FQiPx2V^ppFys#;ZZ} l?5NW@p!co0+NwSTTuz6ZlbtWb@5$i5#*;ET+;3Sl|d^j)5Z09aPTm` z&E@+A$!YF@c#bz4QXwm25%Xwg?+L_(%Ih~LAVRxx;vPDD`@k#fq5GKa>NS&%SU39R zl(FF#-s5*=K|eDLz0z+46OL83a(wY|Cg9sE0{^PuKmDdq2merubPKOj8$8Wnf?eVp zvCrqm^}B>IU3$0Cb#CfHMva-ZR^{fo;CS~VhAM))B>)}7~- z_q-V?QdS)bgH)VwHAcpuSo=+szsv4(?tNR5Ch-s}urRM;VvnHndTP;$QX=M_sE(j< z`s%FRa~JzY7M-gDo;ltZT>7##_qbcK#XUkJ;C7XK<3jGa-Fn-BZ3$;j3SfO$jD+BU z09|^%yA&OX40;+LFw-8Vm8d2>2kFA`n<952jH{-7W+d17TiFg}!f&)w=e=#Nf;fM8 z%p@0cGrweIyE(YiX1?#Yy}aMfWo5E+c)sUmzeW5+z6+#C^$2@O>pM$A$^U4LL~it= zg=|Ky#)|q3Q6f7jBTlX{5T*rm1$;C(Vqnk^du5) zfxc&l<{Do0qFrDvh)sO%Puv?Jrs*832z^DUk52^{IIcnV+T~z<<@c{Z5f@eruko#% zI<;?371j(Ql>VMlVKR@Eww$bu1pNR5%5oV}P%bTO@e03hkZ0XEza4-nLgY2YRJwj@ z`Ad3md*W1|e2+H^wvYs~BTH~6%#=&TN6PvQ&Q+l6^w;Wei>Ve=0?ZEd(}_h=ap+;% z@U|*mT&wHXW*@as5?$!fJ?K;*Haj{o6kD(|W>ow$ZnF zE2dxD#8>N#lsF+bMu*<@G%ebbnMgSF`oO*#?scCI`G^(RXFwLsrPfFks~6+4@uLaq zob6oi#+d>H>Rb6$RGmTkYBw6h(R=x(MDKVTVd-h^=9%>r{=Bme9Vl>K6eik*p8QJ9 z`JwRCp((U-V4Jb4dJ7i7Grx1g=hsjvNO8B!t-XEU{mGM_eM*EF$EgV*G42x!2u|wm zbWvwk^(FP7t>=|Ezef^C?CnGVt2Ei;30eQlyIqC4S6Jh5bxICX(@*McR~9qBsDLK;Ng{fsaTd9AT~2;aaHG3L7d? zOwnw0p9H#R=_#&!eY`z5T)*GXYk1h%Kkr_bufALfZ;awXil4Ewrm#+L{XQ`Q>|9>; zLpG#3+Wy|}4=b474(DY5iu&^yvA^$6*MCraJbKgmkYM_o`k2<8$E3i;`^HE$V*u@r z@ZN1es1Oz|pz<_O^7x{B6-F^Z7;g2_);aHiE=dTX6z5%TL-!TY>8Lq6A%pJ(fB)6u zzQ|$HsGHH=?fbp^YtRTevf;cc2D$|wclG#UKT{+1`?7aa?X5F8i!;?qhvoj8&GS73 zmLG6GZW{T4pZ|dWY29g9G@v2I8=>dt?ZtG;nD$+(Q{|&wG)&*UdOu-!mIJN~tA7lM zKH0mZeBY|q%)8$Ob?zFbrOF)d565H)_g#Oy>wa%{e?RJee=YmgspC5OX11f7Snfa~ z#rcyNmvcc3!St;}HYACzHsN$b@=d$4cOt7n%ccg{;6}1$ZgQBklMu4}LC|9rN1uK? zBiAjdc=g3F{CTl%5?^mT(!}o8L{h53;(H-13>|6Y73h-)vBC=du7|llXjzj=*HRoc zSjK>ock(6@6kjHUoRw3^<|*>j!r7r@?u3)Ia-`^{d99}PIkTdq);Oc3o<7d>ic72W zNoaJE4PHxzxFyF=bp`u)?O%~A!+#=m8iH{#SWj6<%xf-0NaCiG0J3Dd3XBZ+7221B+Z2x5$a+vD7>Uhm(e6L z$d_BiVcstqAVhM*yPSq$@TEpaV1i8uvR1e_jw5@)slgELLloR%^`ADJ?x5`w(wKb4 zxY?lH6L0kT@))#0V@NSmpF-g85@&^gXD}q&<$G)~HlVE)Iw!_i99;OOJuW_DGjz>| zxZw;E5a*f7i%*_~OXjocTGWJ21*DpzWF19rE0<4A5`5@@tNqFzd~JF*0EHMG#v^M2 zvQBkM?se+b+*+vqm`15QuCV4ZWK-t%a?N#&tW3i$_sdW~b0Hb!@ft2&GV=C!?Bj#% z+7Er0bzOceI8kMD$Gj7}@MLs@| ztq?`;a5XGAQhg{Q4xP%*!=};|tj>81Gd?9-f2LTfW1UzrY-Xl%$lhr#V4zJ!t^{1V ze)mMzUqy4sGAs=j*C}-B#BgVgMtw|oo6cZ8+_9BE>?ds9Z;dug(TN_T6W|iqa?w|+ z*SKz+|3;f1uQrF4&zvTW-LmdVI2OFIVdT&b2mfOvF-7iB-4l^$A;g<7gNyr@frp{2 z81T0u>ELij27XN5$zcRLaUNq7NSfmxDZ7G~ajp_Ao22Lk>CmwZX8|4)1ixvnGUO9A zlVSxIs)YORal1_NUtZC2CguIO$19Jq=O?O)u)tmx&s=U$jZ4s)EJ+Sf*Bf$2I&RGSdUGK>6IUK(9=BVKy1 zJ4mc+L_{2)l6Z};HeWTi4xwy@_OWC*9cry222yZEV;xNiI3-tKAF2qj@qQI(j=qc8 zk@_?yW7C`We^=uB6xlfr`MD0-7#UKIgB_KI;Ip*5zIMmYgLzH2#?TWH=#E7vx$FlK zT#lOZxInb9lFqsscug|`%wJ{zdA8L|y2jdYkGwaH6DED)zTQDqIjWSz+4gYr?_C!F2)ln79Z<>;O05gg};eMn`DX<$0Mo zCL$I>e!jAB&?1*3y(^J}-k#4$`XW$ssGg42F=W8IgpyW|=0qMmNCxj;#GznsGI z%X5CB5?>#8I30szGanQPVEx0=M)s*`WCB}I3|2-UN}4;JtzoD}nv!*6KHAPLWXINE zC~P%QGE8mQ-~^X~z#|^Xw^&rtIKikkR9wfRm(-6-Z&w*>$;XhZLRLb--MGJ!)Cze$ zB~{%S2FSM&K%`q`d@vzko*|BF=PA?T;}pxr$NmATo9v&ki(I-i`Q)c`WMYcY%Hf^n5>>^c!3zql z*&wBz;K9b;L^P+3=b<}6=Adl)60du45Q-(*H9*7gvACQea=+e(;hXcd6_+&~i`)uN zX(%1VsL|R2pFBgcNs_+^pERaXbF#HF#++U996Mu^=eG1Ji&>^G217dGO`8KU7n78o zK|Fv1*P&cgL|Yr)?yE@;)Efe_)uBC2h;Pz5XH1_0au(Mfz*;DG8nMpBEhKJ;is-xk|xd zK*5*ksz%ouVu%gAb!9r$uT!E3J)N4aqL$s>+V-dQ2$b7yjkp0dn^ZJCdf$QCL zSN3ba&-c0&yXvT1oOq)!p=J(i9M)`GX>eSHFIzW&I~`d9$X9Q#{pG}4xv4C3#S23E zOxZK)J%%Q8l@gfnj>6|k_`5W+!43l}z3@ou`~r6m3}%kmjijQ|kf=^?&#rdj>gUV6JttKTDj>+7C&b>SV`vuK1Sy z+JS|AF0S~-dpG?NcY3b*v@55vTUSbi?doJJdPYAH?Hsm8KVdRAw$0nF4RXTywJ+u= z%s8JQXTwNELKyx`>?!`aKBP`$6S#}EErjtCwbY>IHR^;(I*C@sL)EQl#-;)X+S{G} zJ0$MO0Q1^EteLDYs|r3_nwNw?d4C80>H`{wV9cTI$=$7hh|wdu+{umcIJiR{(;aww z&}oOgQubc z7g(+RYO{)>b|A`1J!J!il`b9DM(Vf)4jr*a-=2kYi2l&V8^>&d5CHC2{E$6zFoR!j zNQ6O5;3a$FmQ{onS!ZqXDQd>6`E9dgXpj3!6>D!@(a}h7q&JmTL_m={oC~v?@B(yv zQr)1Z=dJ~nrZ*J}n8Ks?>;6=i_uKS&6*q_H^S1E4J%`8lZCU3jW;*-xc9l2c`{iS` zp+wgA8C9(VUpc0?wd`+dKp(41zcJWNWp+@@;{ZK5=& z?;9#{Q+`Q7%J$h@H?X7!JDJKwWe6L)G>IQGSxEzjupY8t*?zN-(=nnoS$NVT%arWE zK1mZ||I}=dnaMdYw+$(aake!Wwxo6k(dife@h_vXD#@n0fUZ{CLHv{m`cUFleiw8} zmI3uyM?`#FB^<7|llm>11iaQn{~L-Xe5Y>zlR(y|fUz_Mc;4hSEv#xez$K;dfOqGb zDu+96RQ8nduyOMDCS zV4{t%=mO-25jjSZ4R}YNP-yucj#V-=$!r*M0IT*U*DMV#>C8tFb6fCcO6_qM6%P|@ zh>#orVe7~cm|PgMI$fEPS8s2WcltB%F+P|HpiRzrLlHGauATr2Yjx>OwUS(Z3tTF8 z)}H59U56rQkNRGH8dq!B@4L9#GDzK|=VN*$xJFvLBstPVM~udv)@*}&OS zF(erQD_-iJ8{NPj^C@0v-;K(tQHtL-DsogIPl+T-r%SiHUZ@N8LEvcd^$ah&DK=<( zXUtcb2Ls`c(;%@shfCp3Df+QZRW%4JVX`4hHF0Fsva zdl&E|YtYQpEK)M2vy1@wAncK=+C6HO0`h7a_obI`hQqLS$< zyo%$cO;JBG7eX1RnLh}DJUsJZU_WW9xQ)V#sBuY&8sR2^6GNxd13G1bkIrhHQd}E} z1tzgV;trFJAxNiRNq|^a9M-9u&iG1UKz+pH zj8p7jLL{RC#3aqYFh!+BX3L_-ovCaL=G2;yVShLsnBDjy%b=!LGLdforz_D~vmj5d zG#tpcrAoyqAq?*GfjxB zJ?Ero@2i~*U02*(=MTVx8WtaSk)Wo(L_XtZRXikA!J6}^iQ}t|$Z5K!#wj=~*=Fr# zu!S38{=t{FN_t~f%BnudNqmWuYT}0M?uc3dmv6+BdD`C{cZSF;21TfKRg){BgK^GH zf}&pnlhY_K@EE;;=jghXf4mgN1?6$j^1#^+M~?kPDDjx0r;ge@A|%cG0~6gn;#WXN zP3-(EN}%JlL21OAp|oNz1deIMmOw}U;;F-6+tFqG%)ru!xl}MZ@YhS$>UtT!2VDmk ztwY2w4xuHf#=7YWE^W4#uI2W)vb$c{@>}u%Ocbn|QK-*)H{?+a;l1}+iG2OK0WD-% zcM;I}u(JASKNNCVROsU-cG7Pa5ZeFr+D7OUTGyM!mTh?Ny393C{k4gieAG#aaSzB5 zVyG&n@1iepihXUSvO_EpfuN-h6X=^q-)4U4MC#q$5fg`|S5=o!j*3e~+E+c3jIb&0 z{8PmQmvTuQ;2T8O+|kJm`>_LLRMn*Z_AdDp%Q2-ruf|0h?wEHsmn)D!BatxLVP+yK z)@<(~I+8(dA_<2d7=uH67T{Ee?95M7I0rBCr?gdTYV!>C4x4-=)Hc4yQvpWkhTP%k zmh~XaUyrBmJ3)qr!v`4*yw5jL6c0@j4Isv@29qYQG=ef?Z^m$^?JTJXG;2f=EMTV2 zvdc1_XFNjq!aiB{nRk8a4y1oWaBk02Xay~dlJ`3VCHy3rKiYl5L@MOnnW`lClX^&f zLcKF@LPf)T&V!mGsLQuw3t;I^-Hl^L|>OD#52g9&{|o7mq&@v!zP51Bum%SrCQgh!V&5dMAy zhks$K9LbhE3|G>+8g>};T7euYgTri|wV(u^A!AH@=?$0LHHbNZ*MNU2$cUzSTsBsd z$d#V6u$D18HEFl+A})57*lQ6p&L)!1;|{Gs48X8Cc|zq++SOze2m7;!FXT-8VCp7@ z+$mTVZ=cnT4miL`x*W@f+`b|oRBmxGvR}^m2R0Cl^{OLGh1*pg(Q6{Cjl5{U#I|T# zAIN|jfLvAGlB?~|YmeF+BWq&}Chd%oP4&wxdOG|w6jl{M00q~&nn;q@f~}6+Q!zal zBcti{xlOu6>Y^+VONH|=rC-Cq+@CYtmRW2xw^=7n*E)SdNzzg5dU&f(>N&sIjQXu% zVL3=-Kpgcz#IWRHk=@1HZRNv~=#>l_RB3|Kp1dNE=Hu!ib^y7nppR^)%Z9dtoC!_$ zJ=6xE>gNcUJn@49+yF;F4zyfHK$RY!w6h-+VE#bk=fFs`u`5rRibO@`Gx8 z!FY8Oqnv8bw}R`UK~Xm{maW@MHlqg`q-W|@)4E?>a2<>ZQh5&_WYNIcfNt$sr2^S6 zzMMl&w(sYITqla*XEcN$iBuLMSVFFQwv1QNKd&-UpD-XRw-r#>>7u=}AV?8{Au_{c z@dyb{uG@dS!IbkCbK}m_4F!wAfF;vF*AeB<85O8e6|XI7S#DikGgP#rooIWQy>hM3 zlwEfeEHkjcKWDVX?Jbd*vTAIT&`Ty~aWQv3Kd1uB6iq0fKt5jU5D=sXcNkmY9MlO+ zA=4i^W?-<>Eh|tcjRyDcXjd{I0z&VRyu1bYRMO`0waQ|~88)RzJX09$Dy7*I))s^E ze{hQhwcHujW1%8?*F(eZGC!8KRKSUOje-Di*)A&?_#F0Do=&Z?OCngwvjK#}-4Ec* zn+X|8G$8K0c$N2BHltPksys|eJW7J#i)jK}P~>PtDJ9~V-=;{*r7PD?v0_QNHcqT= zD@yX2@tcEP*DeiVC6Kt<*2lRR_oo`u`~M#Z|1`weOStaBN#fWuJ)kx20rhYWs^`5M z^y&Tu2B)u~A22-axb&I-1BQ$fA(a&c23}$+gMWj86Y&QOd<#EdXdn1Lz`$el4;U~c zf50&CA7J?W4=|)y{@;T^as7%4UMr>UNP;Hj@cPv36evBO5@c^v%&njlM%x@&Q5fUX z3PV=%$RxC=hIV>6CKSQRoOC--yiMXWnnqW5;rbwurc0+vM3h>6um!KtMxZNaT*1yJ3~%B?|%4z z*r=urhS-W%OUM4r?r3pijwiNAYodh4Iu;V8)=|4_M@5@f+Ypnc72DnxRN-wMYw+7o zWU6TqdVeDRfyL;92}{k&qWf_5kfaNzZfc(2kj`^v-bHkMvVljt>^sLj;P3w|d>klS zb6CB@)YoIwQeM6kmPpJATLA5nsg$$Sltb#kL~T{KhOr zEtQYF9(M?qB!Hs)HEhE2-T*UNEvFR!MgI5Xh8^+NqnF%)0^&c+yP2%`R ztfzybDhKj~ZfZ>u!*G)&L~j(9EZ<^{AojIvf|W>Io&HN(dOL&kYcbEhAWOqftA( zkHH3=Oh4m*-pmB1H>fULY#^QB$wzvQ!m2ZgDncDb_)%I6YU5d0NBST>%pXL$Uu3#= zhQXx+M+cY!9@@MaEPYLCaYu7@7_!yDQ!UQV0Opt&AHdcbjnHIHsy$X#YhjSV5a&6>U+tqZC>kKNbhB zUJv#IeIg{-2=^kOpY^iCTzYJuz8k^6=Zf<&vGu#P<_zN=rCI@mbAcLf*HFuj6nSkX zRiUylrISA58uPMfY@4Ptey#%%@T88#&qE@#87N7}^i7i(30b7+Jbv=%t}}9uxutOq z4pZ*0emnHyjfl|bXL}`+Ii(bXA40BV_MEo718*>)A>`c$HAji@S-|lqoy(XMxs`5O z8&VwYwNpeQ2Lv;(>W-ih2wb(HR~^ZnI5nKGyH*g+P9k9rIA)dK3>!zr@fkHu5iFd- z(W$rr8c{|sV0qS0p@7ngw@P8hUk{ztj;D@BbRJ+tZ`adn^)ti#GgCD+0y_F%TN~(j zYeF#p)>guF0$S=D=co4kt^s^yg}$3Gfs2 zXl?x9)nM`>$BqJZ{}eK65j$ggn6KcWgW{(7tcTA#9uXbCrlIg+HLu!Ch;{)@5hv2c zsxUCL{5!I^&@lF{v3|baz()X9&0PjA4_VQLx^eqKE9;F3mJmIZu;N8S2?f7UoH1Fh zW5?p#b_dj(VEC$PG0iDsf$D@EKrA3Sj9|`omDA_lkT9)Tu8m^5n?9r2n$-Y#qKC1A<+XYoyP9wJ{P+YLCqa z5x2`fw3u#(EoYE>6XMqi8WoAJvlT(2KCk9U!UoX-!vW%>DTAS9_70}SDmG9+#JnfF zO%fMpXV5$v5ri)W8IcHqXb^AEVDM|!ES}c+$o;Shh7RmjK=tv`lFm1Itx(9VSod=} z;=00vk{6!)0E$F3b4q%+gQ^^9(b!>sJT_%8gJ8)V13WUp3q8%$2q0XQE@&tdedan) z_0&vszL%N_Z8f|KFNAdNbc&$AbY0$5P64^C^in4u6Jwn3*U)ku&{fX37}lV$xs-`v z7}gCIL^+{{SynN;!@hB^doFYH0$&A+BLWjMaKuAYf9ZlLJ!uBlpa=s>8S}FZAZsT} z#l1Xh#Sx3K(hEt^Z8CH(;%x}_Zn%X7(q~G)RZf>6$N)iE_!+2qyOqJnb&c(AG+s3& zWA@D+2lA|Nv5LY%FxYTRA)M)FS6C1RYbIx+%O?-giZm&)G;-iIrV{vd2)uU~6RW^8 zXm{Du5wpiL3xDq%i(>_ibxULXC7D~QB=%4t1lk*_=4!;oZD_@E)N=%Bi}@mcE9=XI^#PL&XtfF&JrAO++5F<8d+x3A zYwVX26Clb0e{jo_{Rx#UYJxQs z99LNAr%26;00tzX{b4P{6%1vdlb{NF^MHco#6(Cc-GFSi)7B$O?pd9jw@+faNma@O zp~s&KZH!1clvb1{C3q)8Dp4VyM+cXlAhQgh-)U6*+zjk&jw1>3x-eUJV{6aI*lC&U zOV}36EFzWwp57_Q1kW7c?aN7K@7cgbEQN!8>+Lgt=oi^nqIHk-Fqosn6J*4PULWP= z32X>cxUx~B?u3a&qAJv@$VuZKb{X0rgK%^>tqqtL=1JCE*0V7R2Ba0zlb8dwJI07x z+Zfg?!p&dqX#Vbx@wQh!#BdCn{W=Mk|LGPGvPWE^Q;iEE{b*7r`8J?x!7WnB!8JxI zmP{Fahx4xp&WsAocYi9h^8@E)J3ebjG1SAc}Tvl>kA>n`OQR}pn?GpmQ&-xL0PaNZE$ znb%0_@)j&YLW+10+l78=rf9D+XcIPjQ$^2=_a_l=Xfx(v)oMxf;5vQp4;@lWp5bH} zkzlvTgB2=1?Sl^0T_b$bm+vM!-8ccv}>HV?@dl0`*6O$#5})Qfj!^EjAet&$BEFURxtT3ZQ#d7!!4Dh`;C%uYHjJ@zOdJU!O&P{GCYx zW&}HsvJ|FWiDsxlIM(PNJL(Zv3@eo@(`Y{FHVMSAm z+>MUYNl4W*%Rg(SC00#EpgLN?pe%M_kGoMrg|W86f;rH`+{Otg3wP0?!m6k}%7}DM z`xkQ&gmyeD$>6C%(ACDYK3rkUB0UTKG$aa%sa@+tst#Cvn!S3|Tr6X$wZ3|Cgq0Q8 zV665$}3wN*Aa|MSk!vPub2^-+EiDn&-+Bp=ityv4Yt>*YV=#H(tCWg z*$@kJ#v}3@e1-tOMeRURLM_Bon2SY9&C^zS zS^clKHFae1{#o@zMzg*$8)}!zmz*d|r+~U3S`*V164ePpx-7`+k1{vAuLa{3RpGH$ zMOCjRP~UPeW<;D!_iQPGdz~$y-VD>%La9AytEX8QDmVm8#k{T|uBI=$E+>_iJk8C` zb`xTs+FmK-hl#K3kqrB$C{g?jz~+;?=z)9+bO>vWkr@{FTZkgM_`lY>fX4)Qu9my4 zgBVII6UVE_PZb#=`_AiQp#wx`@t&Q*_D6ui51fkz(Yy`Q6rRiON`c*pJ>%RhXY(BiZ zE%+nDErD%zqFHNdeoDL>h5mhk5G|%0lIxWTs1;+D0_PisJyyV}69OD{9S5|G4W`YN zXF&vQiZnFdfFa>sBaIZXGO2UVN?4}v7woDD8%k`SzIcjt>C?u)#?7ixqd zVUx!CX>*^{k&?B|1}^6Zwj=w@+Z*D}SYkRjFwq4i7EcjE@oH~j%RGOwwE3|iVtiGL zR`UN=i4=whp2-|5`wI}esqp-{qMGq(S}t$=e)O(!7b+@bO}hSo+DL=ioiEB6Q~rwG zj-Z27^+pdT`#v?A#8$-u@kmA2-4Z?z&z33i06i_H`TR9oUUJfY0R%)h?lz;nEDQOD zqUc4dGOJ#v&%-*yYyf(dX>ry>7(8OeIAJb)sqH@8xVrC63;mz8zO zjY*U|I^5oj=O)%OX9(x=SHnx+G6 z!I{5A9h?Wxws@GSyUJ7TuGmA_#Xq>W|Q0Fa6)=i`H1T+%SXFySk`yK^{%9Z*M;=^ zt@35Qfwx$~Y4v)VhjVJ-`qcm9!O+!(ty2jp=lwG6P(JB?(D+wv{c4bf>Q}PilSOp~ z{^(fF#95*%Wk{yBUT0UK-O!x0ChydnWqNo!`Z#n-Q?t}6idZ(S#rIa%<)PVSjXl3{dLhLDqsyGJ-(9oi4~ER&|{tvyUAjVJ0F^VT;TyDXhHr7wKA7ZmSlskVO0hGv883LgG$(Ty~Sg7 z9G~8q5F!C?P4_V*NV(KqN?MfhX)|u}#HlhWR5N1C6so=G3Onayt3%3i%j4E5)4C3Q zGK76CG)fcD4j+EVT-x7mxuytM$eygag64Xu?GX(!K@S#MrP zv!PFxdfr=hUV3**KLqPY!!;TW$$4K)yDdsEZj*imR(*4R@V)Z@O9EU&6S)8BH&Nb= zE`yrqRGDuZUyI}>-sq=kx{QNS?2;m+a+cXFQx{}1_VM{Q>)WN)Myi$H;Smba0rPwS z6MQRB2#Hf`-2IS+TqLR+3s8Z%Aoic=h|U>)rhy>%cO&voZZ&kHJm3pesQNm>!VENq zZFS$)W z#b6gXPd^22{d?c*A=2?ZS?v&JY#3EO!zZdMz9{%DGwozzfWgkh;1lw2r}1~mB^OA^ zC7bnd+8>l1^b*5NxL$ghVYcT9DJRzn%brd86y^=hX_3)kXnj0e_W^MDzH+poVtZFP_Ve>bU z=)ItJyzmH0U6y}5`w{z>-FPE}sAhxWoGpsrOq0BK-^Vota1(*P3B=*9*02I{KnO59 zaD*J*j#Hj@h>hj18Tl$YlpG0i$PYo4umT6CqQ`m#)?0i5*UvsJCHDi=HPoBHi!`;^ z)jCC{&L$U^z{d0Hp7X46w~Ki@rJE-m@HEZtbF2q@vh0FYfq>QoN22kWxJwNoqVcJ` zno&aZ==a}ZrnddjVdBuTU1()zt(-7*oR1$^mF~UT!irIGD6!`j1dmCE0)}vULG#i6wA&v70Z1wCkd#BDI zTvdFx9)q|nRc071^`|RAcpA{&CUkBZH2x`w67~yC!1 z=Tfv%cIAcG8Bn1vpGtwK5QR*39^hfvxCefWKLx*e!AzXFIn&7#d_{!4gY)oSV=7>F zbk#1fZi}hHqPYf+qobwd65H$R9<)yWQrivT7of(YvId8oHp!dNovRLto=tc8E;k}q zoh1tsZ?Dy@svHmk4&>$*@UBTInUN^Ldpzqm_R6r|6%0Y8S{&H>LMH0kqr z@8~B~ZhPD>r6Kj!$S}!*S3WbFd~GKS_`PfbLamF02yYEA*W=c*-p#Cnc1I%Lvny=e z#jJve^D+RmcO_VwCrD@+IQ)Ql!K0p2{KK7x*f!Z`NjUpDS>wR=e_TN&j>k<0q`xaA z#&)i`Kfhvs0$vx_+`}LB57KVOHJ4_|^q1DfJi(9gr@V9E1IgBQec%Hob9LRg@goZk z=F&Wwqcr}04>B7-A{0y$I{GQ(R(c6 zG)w`n1xqmsCawuG(4XTRmGESQepiZE_w3{hABCxbQO7_F7k8}+k0Dx!DU~x@q!br# zK>u#Q%EP)Bz(kQvq=`Uc8>8{R2PUolvsQM79NpeMu>ij!8dm9}$z9Ly`3!LY-PmL) zruC_|V1O7|;`tH#o{gfr={@pp=4_g6gN_;&uX&g$W*Fpg{>iAa(+RNzehqoiBc`9p z3-KJV!!3w%t9o1RB1X2+=l&dU;LZ7Zy!Um<`Mf=y$Cdrt{eF61;(KZQ{c`am0)95U z#7yUKd%iskUvK%mf88C*?yd6rM#kT;eAoE$d?M(41zjAhHt_mLq?=5#iXZ3kt^PKq z9(-`gd17{hn$RCLFg2d&_1C~C)E6&0q$kLr^Z30i{g<&N%u;=!a@jpZ07tbgm@Ye7 zMN3eT6(XH;mK`ZdbHxq{nuh-v-sy_3c;l}U7UfCAy9%~L+kXaN+@uJ1{D-{-F*N6w z43GgIRWCztutK;Ub!bXci2*Nl3rA~q!BDk7IoL*xXH$pIHjJ+_BU}uDsh`x8py8e^ z;BO;aM-{=c*-yrGZQ12p(;c5v;u8idrnzvN)>^RqY!mGOsa@`Li&2&)uYt>fx z#ec%Ex`9zYppj@s&z7{N`AT}-#HSFxg0(0k-6GSaWU(1QzKvqOnVr$Fr@KQfhvjA6 zIJm%&|FS6Pb-buM*MVdIH;;l)9;RO%;XTidjj~T2fzeK70Q<+K*u;}*tN}L_PW*8x zGEsKbCJmbj8sl!8(D7{BN7_)r)IkhD5X`*G4x|gvsMRP#By5Gw5Al$;#MqXO3B-^) za4BX4F>+m^HI&1K*uW@kK$*#~9gv(86wI@TJM~G0yY+3EBM?T3#8<>>)5?ZrI zt#uks6CjG*>;M7rbPZ*sU!FnJli`16rA5nd!8LB-2ZtLSTqUYGdl3iz7#x5o;bB<# z1-2;9JN00bc^y(hFlhKA0~Zi*eAiSWoPvg09v9Lmd3d6g$8FXrW)4N`s!ATxuXE?) zKOxSK}><3X!-q&0N;(vInj13epdWMF>L_<6&JPvtlI%n z%zXDBigR-2&0r zu|W5>nzu6r8*k9b^|6c^&P<>OKpLXNg)s|Xe5MyFuXs?Zqc)(2pJl+SoPz=e6zdn5 zs+nPM>cG_iqJ#x4BfUAB-K5U?l-A(u2t{a6UZ&)4;Hg_j)@zXI^e+!fvZ2}pT*CNW>(yf@fYy9 zN4ZYwg+{b45?jgARoW3S@Qzu3wI#d=2HG{==IHxxnPXB!aWwoY16G+HY!%If@y^wc z7O!cw8R>4%)Np^BnzX(m8S#od54r(`0Q36zJ^F-r4J_iwEq(LZ8eF>H>13&v=~{a< z6>f$eyIlaP0a{Vw*iEznh*n-Ba%ao{{R28`RNAtH^Ys1DJF)}7G73ey?nRFs!6mpk z@Yyxn_%6stQg!5ALjmuNr^)r?2C~HbG5L5Xtojd_> zO&&h|<{Bsoy=#6cvwRSqicf4S+S;hZ3Cd)m1}_<`mV1BsQDMbc&9cI{z zG{Ew!_Z$|&X0jE6NQs{t1U?x~CK_{w1V<$oBGs-PvzdfGu_E7i-QZ2Fj+W4`XEjIM_7-;uL1rR%Usebji?kw8?m=%=h{EO0H$sd%4&t|lnr z15~#BwtzV-hW2IZ&vh@@|LJZ#;UNFp-5_zS`XBMQ&Sfcj$o^1rco8Vh08T=zP(Ng6 zeg7Y1L~UZzu@1A9l^n5wU`M|n6f={^r)h8xs+?jpV#Xm@f9-e!!t~6v52@!I(+EGk znfoF2HXxB*jpDgv` zK6WI-Uy^o#7j(&1VGa4mL;^f!R~VUHDOOurlof0os9t$gxh_Ct!o1En*cvILm&Z66PoH44R51x|s5lU_JRjFbRC-0N`5=s^1O4%mGpW zU}i1?z#1sX88YIx{tZFS8hKft=u&AyeSrvv#~g#Ag{6hSZYq2cy0=>iVJbY)qmJPC zNmRhL#QdIxZ~;T`AxR?QElDdx2t3C88+{PR7Wp zK~zQTaa_u`68<&(%Meb+w&P&>(F86&rF5dKUZ2-v&}q?9{2Ju)n)VZVi9?0Mr{HbOw@8IIB-6oEh%o%cGp zmp)zGMd?G-xTZ?gJbSnE|0aFM13i1ksUo!z8Q}5H*UmChHAeof30?Ms66!mL{N!MA zn=s>>?YrTCdXqx+=#&Vc?(7`uT*Ao+@YJ6wigD>qIP_Y}#*#o$gjUaSe}~{vb}%WJ zR0e90s;iT%&?L2G-r-7DGA?FYK0?6^XOC4J+9H3#>=1%viaj;p0 zNQlq_pkomOeo}IF_iA`vdCSJ5n5UE?@g(ewKzk;j08uy`NTmkM3)Y~IzY5;Ib0f`Z ztC4W$HeoA-SNkr*O?<_BRpBw@6;)Zd12UL}sTi5^)Ob2(uU*8m?9P6eM@?ih}13|KX5nzJ-Fiw@}&&2M6=1e*qz&)O8)(;p0{{@DEd)AO2Fo3rp$?X3F z1|ib=zU255Js7gq zTul9U{zfvZI=EB$%8E*&%_M5!!(WT^VF??bNED<}jGdT{yJ;}6a#DGFyLb6;MdnI! zzT!xYDWADIy}sr9km5R>8Urud#hy~0^FxKMxb#NHSom#ex-}|W!Ej)0IlkH(j*@C_q zGd!5&_X>&v)|N2KEVP#VAnx9vShVm1Kiy<$~qHSYZAd*`DtCq8jit5qVFbJr0 zH_{;}Gjt9J4kZ%OEgiy8f^fPmx>BQ&?iZoc22mKNcxem@zcw8QKq}}(Xu|)l~Nwie+)RL z$Kt8ERn0|Wfx>*rr(AzIGZP#h9F56qJ;yg`>Y zN*Lnrzkh8KA&l}h^XIuo1Qh#D119U;?p3mOBl+3(*6H+~wZFjj3U3I~knM?17-?@Q;VJ1{=}xc16xx$%Jr@l=jU z_hg$lm$j^1zZ;T(cLbouo3-{}l%P%FmEhY)v6crw5pMDFtiAmG^fSSuJ7uE!BE73& z8wx+D=Y?`HdvZmQCbCrJ7?8rqp&s+y9Xf$~7PTkTwR!uVoHy^7czTu!vrNHQFJGzf ziGF5!Q(Qb%;Rc*VpHR8H&!NXlW4d zlgqQcA1aJUYUIc#YpYeM22~EcgCOM?%ITT}{MFhb%*OZG;&5DE(D!L{8qM0K289ir z_Oj7)chlMu>p(2a=%0yMB}%r12UQo65B-pbN*RLtzmLd|d>?D+vRRwww8*UxbeN^h zcZ$uL?lN5Tr{a`dt#Mkc-kYgrXJA9w2$n)9=*gc7`&FMt{^F6>n|Rp4LryGKU?pA* zmn?U{pD0OgT4U$pUp-;*gFFe%TUoupFO^ZckyqH~P*i7084d3g?I(yItU`d9R{$o} zBJW1V9fK6lA&-%^1VT8livqQya)u{Ns?4;ZE$+=GRFt@>mGv^rp`*j!qIiD!;BgTS zyCammKrw4rPos%Qg(|*>uZ2+HM2O=e`5kPC4v$`n41VmJ7w(d8vYlW%Sa?13>*<2{ zuNHZBX08(Q;7hTXxNyo^^_QGP@~VdU3W;PXR8VSX;k2_k(Ey?PlfI zNco6|Z-Mih$yhqOudSZ+N{L;4^R;u1 zJ&0t4#$d571ES;eOCE$2_v@t@QlX1^6s729Z%CidSTj`3m1b;hC)N|r7Ic)4s;I=$ zONiJ(!6ut2pnV1vJtG}x61{v3B{ zb&zK8y`$*Y!1_q_=8M(_L1UaByRG?T&9jm}+A9g`Ek$U=m5g+!+-D@s_-XEAug!s!Sd zmrL?wSs6WYRS(cLnhMv~QR`0khCeX4owF?np5|5ZkTZzGYztThjUfFNM$8}oY}9Gr zJo?JlW?S<(fU6c$_n-n~k0lgClS-3jHxeX4+B07I*{d^=Dm^JlE9|*4oGD>kspnRl zB3xMUS};L`g+Yq%p~=yzmxOQ-IRmd|HaHZs7dXM}1*E3(KGxr}D9g3KYYFL#Zb`^z zSIjdZ@?CWsV7kt`$;)8DB0WD&@@cN!MIz$aTtTb$#m;?-@bj-Y}xQ)9Yze5 z!tD-pkw}wFzWBu5Hff*t{s37J+jt&zS~w{RwQE0lU|o2x{RA<<}eNmXa-be~bxKtlj~%As{DFVV!0)9uYn_Y=i4^d=nGf9HGRoUAW}k8yfoJ#Z>CBXCTDb(btA)1|tCydLsKj@^k~Qn>EDCK32iP$l>B8^fkpr-Dh!>T%B873=*%HPn zgxkpGQ#iidzh5S)P2`?st|j_J?zvQEbU67>6;H1_yBQ-XEP#yK?cu$blLZX%GA0l^=5Q7P<;FnYNqFxN(S2lIQ0IA?u zy^P5&#)aULkt|*QpRB*l;5A{e3hGCh4JFKeuCXd)-+q|zbF6G=d^_WVZ)!eUU*pr9 zkMAqq|0N>G>crYnr-Za>r<-EXDkKh9`yMOeY;osJuDVknR7~LNQn2W~a@O}4>H7De za%z9$N0)U=*di?_qE;`U@1B;&z`r9bZY+i2#bR@TX^e`z^Ji`XWi{eB#%vGlIv?}j zg~D8{b+U2I`ordOI9ld+iI6=V5j`~&uit#2cpi~Yv!63l^D)#+4@N)z+#`>2>$+Iq z#P-pmGpBUY-s@ss#jWHvL0*k?c27hQi9c&#S$s9VuST(eWLURt-?32QXxy;lhe!;; zM>t$#YGW;2Y_A57eKz(bO>g>>V^$dDxI-0CLo%!-+$7IpgUE5JLXUiy-Y^X?fT=l# z^}cj$jy^5UkS8Hd7a8cPz}h5g80sI~5VWrXLw+uK`Y70i^tM@VXBYi!0fKC1z72YN z^+wx+vq+nx#dM!T1vNuSlgL2>`$=MA2mEp3k~F6-O{a!zox#2=guObjJ&aCbC0WJZ zqVnaOYX5sb0@Up6Sgr~~{8;WMc}k5pPSw`EXozVZWgJ(4pmeubvlVmbwwez@6VuNw z0(5uqX^&;Mtit%ubwrDk4u1V*96URsxxWM&=CP$Px2R39v+#|c$RIco-7GnYLKW1u z5HIfREr~ul3?sJ7{bGN5rmz$Ta|RL25mhOsdvnt0b;&tQJ&kM#jaGgbku>d0NGd^# z-E2bG&p-C1#!_~?u*!TW?u~ioax9kw^10gT$IjdWT4&>E3WMSSnQn zsFI1&C_w;(!yoosEluCnQ`egC1cR9?FE%ww7vzqPEZmr6^lHsHlY1tW+fJV$kp!!9 zKG&^$y)-kSuTwWnZwFfAR0GU)}hkC z<9`xWbFdmD2(A=qka7$7l1)AMLCgO3#`RX9NrAkJF{<3uyUC8=i@T~pbVY=Mf;n^nv zzU>EyH`o4VcNZB*nyxvIK1RZ_tId^XpKDH5(uz60#u`>D`6*BKP9^-=`P-~AOA}Zi zF6A^|)3hqS5rBU7d5<=)PG5k5YAS{P=N{f$Q^sCmBK_!&BfO0I(!=Q>KiUFM?4$`; zsz`LQsLLdv^=%xLe(a!&?Q8*5g+|3OsV(*i&^oldi4TmFJbl``Cq&gykfB{;Xd>hE z+ZST=!aty+>c><3Dn&8#$IkO&S(@&EKjhpJMaB995XAU?LPVd3UpG%Hapv^C#{FMD zxJf`gO;W}OYwMU}x3XjC7LB=BK?h^przx5mm=hH7$4FbLnH~K4_2tH_dEf=cN9#%B z^uK(xnzxl}3a~Xs@&{lMD{)g&Y0V$ho*udu9u0*wIDu^;Ks9+RQ=B#9FgUMp(g;z6 z#;a+@*S3CHJXt(Q@L%Rw8LDc#cgxiR_;Fj8lh$bW(^F!9h9i|@dFWC`6!ulXOF?z? z5t5MRS+@@Y|4JB_ZCKhq7`t#yj?PQiZsVOes-Vc|D@S%rwC~Vtzq#jU0lH8IXioH9 zqC3Rfu6FEr$AVKrEc{t*@IM~hT4~EjHJjL($b0ezN?lE;aJ*d7r5}VLGBa4nosGHP zh4l6b&1L~%u*Eabi=X#456Rt7kQ!mBHRpEvjcOKUH0+F}9eqP-joenaQ^bHaXh$HI z(I-F}?)mll4!VT3nAc}zd0kZi&g5S9NLgC>BGk&!<;~1+ZDa7&%ErveuJ>}V$MN

X?t71*{IewHp?5SS%g6)a2fr3^<+j|Ia~H5!0UKh$Gliq*-|*JSNz_wcBDX4e%!4l0w)Z=)`K|?@WMtQLehNo+Zg(mvBe$l;W1}RR|N{?GdwR-;8Di zBE4E!=H9EZXFtNeB;3R^b%L`X%=4XjdKPyD!V)UGCl9O(rEX1d}=&M z(yDVvBr1r)7F(u_h12g`#J_aa^uk==5HBTWuqgTMR&5da)h46YQ5>&83}1z9%_$OE zLjR%Sw!~t&YHC@y>$YF~@udu9cChR&@BS|LHu_L6%F!S z0F@VA&KwS-99qB^2mR=|-(Iw{-M500;r5;O3L-_&hl$?QFV4A^V=J6NU5akyG_CvUXLGj?=`*i zdq6C^`)-N2)7H$Q^Ku$JGsK%up&aojNVOBh_uJNGlGH!*=7aYXzH1;Rhl5zLi?jih zL9w0cibj_vMJNPDTnntcO$$04g;Sn_xsAV$YgvACm?LNyQhHlk5}A0^MVPq_{)K26 z7qIm!@%r)wmhA97jed~on0h@s3$bTqR=`t0R5P17Dq+^&U=Q+$uOW=aktAUgx+L^E zDXK*Q?JB+kf4uqZCU(=$iF$0J$@U8k9TV{0lM9j@!gt=2kY*NtM7~G*oPXBy)1C?Z zkO}bE3zQHRZ0PtA6)(+2T&0=4L0q$L`03sp4j5b zc;T^yi+`C+88lP(_-J1$m~B7gQ`i(6RJf}VP+jtonfUAegWoOm$giO|_Yz2M(SJ:9000 failed: [object +# `[AgentSandboxRunner] port-forward to :9000 failed: [object # ErrorEvent]` and the daemon /health probe times out. # # Workaround for kind dev only: disable TLS verification globally for the @@ -103,9 +103,9 @@ persistence: # Keep enabled (mesh's event bus needs it). PVC storageClassName is # already empty in the parent default; that resolves to local-path on kind. -# ── k8s sandbox runner ──────────────────────────────────────────────── +# ── agent-sandbox runner ────────────────────────────────────────────── sandbox: - kubernetes: + agentSandbox: enabled: true image: # Built locally via packages/sandbox + `kind load docker-image`. diff --git a/deploy/helm/templates/_helpers.tpl b/deploy/helm/templates/_helpers.tpl index 0f61f166f0..ab8a71b8bd 100644 --- a/deploy/helm/templates/_helpers.tpl +++ b/deploy/helm/templates/_helpers.tpl @@ -187,12 +187,12 @@ sometimes after partial-apply. Failing at template time keeps the release atomic and gives a pointer to the right install command. */}} {{- define "chart-deco-studio.validateSandboxPreviewGateway" -}} -{{- if and .Values.sandbox.kubernetes.enabled .Values.sandbox.kubernetes.previewGateway.enabled }} +{{- if and .Values.sandbox.agentSandbox.enabled .Values.sandbox.agentSandbox.previewGateway.enabled }} {{- if not (.Capabilities.APIVersions.Has "gateway.networking.k8s.io/v1") }} -{{- fail "chart-deco-studio: sandbox.kubernetes.previewGateway.enabled=true requires the Gateway API CRDs (gateway.networking.k8s.io/v1). Install: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml — and a Gateway controller (Istio, Envoy Gateway, Cilium, ...) implementing the chosen gatewayClassName." -}} +{{- fail "chart-deco-studio: sandbox.agentSandbox.previewGateway.enabled=true requires the Gateway API CRDs (gateway.networking.k8s.io/v1). Install: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml — and a Gateway controller (Istio, Envoy Gateway, Cilium, ...) implementing the chosen gatewayClassName." -}} {{- end }} {{- if not (.Capabilities.APIVersions.Has "cert-manager.io/v1") }} -{{- fail "chart-deco-studio: sandbox.kubernetes.previewGateway.enabled=true requires cert-manager (cert-manager.io/v1). Install: helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set crds.enabled=true" -}} +{{- fail "chart-deco-studio: sandbox.agentSandbox.previewGateway.enabled=true requires cert-manager (cert-manager.io/v1). Install: helm install cert-manager jetstack/cert-manager -n cert-manager --create-namespace --set crds.enabled=true" -}} {{- end }} {{- end }} {{- end }} diff --git a/deploy/helm/templates/configmap.yaml b/deploy/helm/templates/configmap.yaml index 499e074cd9..205b26f2eb 100644 --- a/deploy/helm/templates/configmap.yaml +++ b/deploy/helm/templates/configmap.yaml @@ -17,11 +17,11 @@ data: {{- if ne (lower (default "sqlite" .Values.database.engine)) "postgresql" }} DATABASE_URL: {{ include "chart-deco-studio.databaseUrl" . | trim | quote }} {{- end }} - {{- if .Values.sandbox.kubernetes.enabled }} - {{- if not (hasKey .Values.configMap.meshConfig "MESH_SANDBOX_RUNNER") }} - MESH_SANDBOX_RUNNER: "kubernetes" + {{- if .Values.sandbox.agentSandbox.enabled }} + {{- if not (hasKey .Values.configMap.meshConfig "STUDIO_SANDBOX_RUNNER") }} + STUDIO_SANDBOX_RUNNER: "agent-sandbox" {{- end }} - {{- with .Values.sandbox.kubernetes.previewUrlPattern }} + {{- with .Values.sandbox.agentSandbox.previewUrlPattern }} STUDIO_SANDBOX_PREVIEW_URL_PATTERN: {{ . | quote }} {{- end }} {{- end }} diff --git a/deploy/helm/templates/sandbox-network-policy.yaml b/deploy/helm/templates/sandbox-network-policy.yaml index 864d4bcfb3..4db2a77e2d 100644 --- a/deploy/helm/templates/sandbox-network-policy.yaml +++ b/deploy/helm/templates/sandbox-network-policy.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.sandbox.kubernetes.enabled .Values.sandbox.kubernetes.networkPolicy.enabled }} +{{- if and .Values.sandbox.agentSandbox.enabled .Values.sandbox.agentSandbox.networkPolicy.enabled }} # NetworkPolicy for mesh sandbox pods. Derived from PLAN-K8S-REMAINING.md # section 2.1 — the prior 2.1 PR that was meant to land a local/prod network # policy had not merged at the time this chart was authored, so the spec @@ -46,7 +46,7 @@ spec: ports: - protocol: TCP port: 9000 - {{- with .Values.sandbox.kubernetes.networkPolicy.previewGatewayNamespace }} + {{- with .Values.sandbox.agentSandbox.networkPolicy.previewGatewayNamespace }} # Legacy/standby — direct ingress on dev port 3000 from a configured # gateway namespace. Only needed for setups that route preview traffic # *around* mesh (no daemon CSP/HMR rewrites). The standard Istio diff --git a/deploy/helm/templates/sandbox-preview-cert.yaml b/deploy/helm/templates/sandbox-preview-cert.yaml index 65427780f3..31415e567b 100644 --- a/deploy/helm/templates/sandbox-preview-cert.yaml +++ b/deploy/helm/templates/sandbox-preview-cert.yaml @@ -1,8 +1,8 @@ -{{- if and .Values.sandbox.kubernetes.enabled .Values.sandbox.kubernetes.previewGateway.enabled }} -{{- $domain := required "sandbox.kubernetes.previewGateway.domain is required when previewGateway.enabled=true" .Values.sandbox.kubernetes.previewGateway.domain }} -{{- $issuer := required "sandbox.kubernetes.previewGateway.clusterIssuer is required when previewGateway.enabled=true" .Values.sandbox.kubernetes.previewGateway.clusterIssuer }} -{{- $gwNamespace := .Values.sandbox.kubernetes.previewGateway.namespace }} -{{- $tlsSecretName := default (printf "%s-sandbox-preview-tls" (include "chart-deco-studio.fullname" .)) .Values.sandbox.kubernetes.previewGateway.tlsSecretName }} +{{- if and .Values.sandbox.agentSandbox.enabled .Values.sandbox.agentSandbox.previewGateway.enabled }} +{{- $domain := required "sandbox.agentSandbox.previewGateway.domain is required when previewGateway.enabled=true" .Values.sandbox.agentSandbox.previewGateway.domain }} +{{- $issuer := required "sandbox.agentSandbox.previewGateway.clusterIssuer is required when previewGateway.enabled=true" .Values.sandbox.agentSandbox.previewGateway.clusterIssuer }} +{{- $gwNamespace := .Values.sandbox.agentSandbox.previewGateway.namespace }} +{{- $tlsSecretName := default (printf "%s-sandbox-preview-tls" (include "chart-deco-studio.fullname" .)) .Values.sandbox.agentSandbox.previewGateway.tlsSecretName }} {{- $meshServiceName := include "chart-deco-studio.fullname" . }} # Wildcard cert for the sandbox preview Gateway. cert-manager places the # Secret in the gateway namespace so the Gateway listener can mount it diff --git a/deploy/helm/templates/sandbox-preview-gateway.yaml b/deploy/helm/templates/sandbox-preview-gateway.yaml index bfbb816978..b403d3a1c2 100644 --- a/deploy/helm/templates/sandbox-preview-gateway.yaml +++ b/deploy/helm/templates/sandbox-preview-gateway.yaml @@ -1,8 +1,8 @@ -{{- if and .Values.sandbox.kubernetes.enabled .Values.sandbox.kubernetes.previewGateway.enabled }} -{{- $domain := required "sandbox.kubernetes.previewGateway.domain is required when previewGateway.enabled=true" .Values.sandbox.kubernetes.previewGateway.domain }} -{{- $issuer := required "sandbox.kubernetes.previewGateway.clusterIssuer is required when previewGateway.enabled=true" .Values.sandbox.kubernetes.previewGateway.clusterIssuer }} -{{- $gwNamespace := .Values.sandbox.kubernetes.previewGateway.namespace }} -{{- $tlsSecretName := default (printf "%s-sandbox-preview-tls" (include "chart-deco-studio.fullname" .)) .Values.sandbox.kubernetes.previewGateway.tlsSecretName }} +{{- if and .Values.sandbox.agentSandbox.enabled .Values.sandbox.agentSandbox.previewGateway.enabled }} +{{- $domain := required "sandbox.agentSandbox.previewGateway.domain is required when previewGateway.enabled=true" .Values.sandbox.agentSandbox.previewGateway.domain }} +{{- $issuer := required "sandbox.agentSandbox.previewGateway.clusterIssuer is required when previewGateway.enabled=true" .Values.sandbox.agentSandbox.previewGateway.clusterIssuer }} +{{- $gwNamespace := .Values.sandbox.agentSandbox.previewGateway.namespace }} +{{- $tlsSecretName := default (printf "%s-sandbox-preview-tls" (include "chart-deco-studio.fullname" .)) .Values.sandbox.agentSandbox.previewGateway.tlsSecretName }} {{- $hostname := printf "*.%s" $domain }} {{- $meshServiceName := include "chart-deco-studio.fullname" . }} # Wildcard preview-URL ingress: Approach B from the K8s sandbox plan. A @@ -32,7 +32,7 @@ metadata: # built-in `issuerRef` field. cert-manager.io/cluster-issuer: {{ $issuer | quote }} spec: - gatewayClassName: {{ .Values.sandbox.kubernetes.previewGateway.gatewayClassName | quote }} + gatewayClassName: {{ .Values.sandbox.agentSandbox.previewGateway.gatewayClassName | quote }} listeners: - name: https protocol: HTTPS diff --git a/deploy/helm/templates/sandbox-rbac.yaml b/deploy/helm/templates/sandbox-rbac.yaml index b71fc5ec31..580034415f 100644 --- a/deploy/helm/templates/sandbox-rbac.yaml +++ b/deploy/helm/templates/sandbox-rbac.yaml @@ -1,6 +1,6 @@ -{{- if .Values.sandbox.kubernetes.enabled }} +{{- if .Values.sandbox.agentSandbox.enabled }} # RBAC for the mesh ServiceAccount to drive the agent-sandbox operator from -# inside the cluster. The runner (packages/sandbox/server/runner/k8s/) needs: +# inside the cluster. The runner (packages/sandbox/server/runner/agent-sandbox/) needs: # - sandboxclaims CRUD/patch (per-tenant claim lifecycle, idle TTL refresh) # - sandboxes get/list/watch (waitForSandboxReady streams `?watch=true`) # - pods/portforward create (kubectl-style tunnel to the daemon container) diff --git a/deploy/helm/templates/sandbox-template.yaml b/deploy/helm/templates/sandbox-template.yaml index 6821ab63e8..72b55ff6ea 100644 --- a/deploy/helm/templates/sandbox-template.yaml +++ b/deploy/helm/templates/sandbox-template.yaml @@ -1,7 +1,7 @@ -{{- if .Values.sandbox.kubernetes.enabled }} +{{- if .Values.sandbox.agentSandbox.enabled }} # Shared SandboxTemplate consumed by every SandboxClaim the mesh runner # creates. Mirrors deploy/k8s-sandbox/local/sandbox-template.yaml with prod -# ceilings from values.sandbox.kubernetes.resources. +# ceilings from values.sandbox.agentSandbox.resources. # # Hardcoded to the operator's own namespace (agent-sandbox-system) — the CRDs # ship with that as the install target, and the operator's RBAC watches it by @@ -25,7 +25,7 @@ spec: # `app: sandbox-router`. That's intended for Istio-style sidecar routing # and silently blocks the mesh → daemon path the preview-proxy depends on. # We surface the netpol via templates/sandbox-network-policy.yaml instead - # (gated by `sandbox.kubernetes.networkPolicy.enabled`), so flag the + # (gated by `sandbox.agentSandbox.networkPolicy.enabled`), so flag the # operator's policy off. networkPolicyManagement: Unmanaged podTemplate: @@ -41,19 +41,19 @@ spec: # dashboards filter by absence-of-handle instead. spec: automountServiceAccountToken: false - {{- with .Values.sandbox.kubernetes.nodeSelector }} + {{- with .Values.sandbox.agentSandbox.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.sandbox.kubernetes.tolerations }} + {{- with .Values.sandbox.agentSandbox.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.sandbox.kubernetes.affinity }} + {{- with .Values.sandbox.agentSandbox.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} - {{- if not .Values.sandbox.kubernetes.hostUsers }} + {{- if not .Values.sandbox.agentSandbox.hostUsers }} # User namespace remap: UID 1000 inside the pod maps to a high # subordinate UID on the node, so a container escape lands as a # nobody-user, not as a real node UID. Requires K8s 1.30+ and a @@ -69,8 +69,8 @@ spec: type: RuntimeDefault containers: - name: sandbox - image: "{{ .Values.sandbox.kubernetes.image.repository }}:{{ .Values.sandbox.kubernetes.image.tag }}" - imagePullPolicy: {{ .Values.sandbox.kubernetes.image.pullPolicy }} + image: "{{ .Values.sandbox.agentSandbox.image.repository }}:{{ .Values.sandbox.agentSandbox.image.tag }}" + imagePullPolicy: {{ .Values.sandbox.agentSandbox.image.pullPolicy }} workingDir: /app env: - name: DAEMON_PORT @@ -78,7 +78,7 @@ spec: - name: WORKDIR value: "/app" # DAEMON_TOKEN is injected per-claim via SandboxClaim.spec.env. - {{- if .Values.sandbox.kubernetes.readOnlyRootFilesystem }} + {{- if .Values.sandbox.agentSandbox.readOnlyRootFilesystem }} # With RO rootfs + emptyDir on /app, the mount root is owned # root:1000 (fsGroup). Git 2.35+'s "dubious ownership" check # would refuse to operate. Disable the check inside the @@ -98,13 +98,13 @@ spec: containerPort: 3000 protocol: TCP resources: - {{- toYaml .Values.sandbox.kubernetes.resources | nindent 12 }} + {{- toYaml .Values.sandbox.agentSandbox.resources | nindent 12 }} securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] - readOnlyRootFilesystem: {{ .Values.sandbox.kubernetes.readOnlyRootFilesystem }} - {{- if .Values.sandbox.kubernetes.readOnlyRootFilesystem }} + readOnlyRootFilesystem: {{ .Values.sandbox.agentSandbox.readOnlyRootFilesystem }} + {{- if .Values.sandbox.agentSandbox.readOnlyRootFilesystem }} volumeMounts: - name: workdir mountPath: /app @@ -113,7 +113,7 @@ spec: - name: home mountPath: /home/sandbox {{- end }} - {{- if .Values.sandbox.kubernetes.readOnlyRootFilesystem }} + {{- if .Values.sandbox.agentSandbox.readOnlyRootFilesystem }} volumes: # Sized to match the per-container ephemeral-storage limit shape; # individual mounts get a slice. Adjust if a workload needs more. diff --git a/deploy/helm/templates/sandbox-warm-pool.yaml b/deploy/helm/templates/sandbox-warm-pool.yaml index 887b056fbd..0dd731a297 100644 --- a/deploy/helm/templates/sandbox-warm-pool.yaml +++ b/deploy/helm/templates/sandbox-warm-pool.yaml @@ -1,4 +1,4 @@ -{{- if and .Values.sandbox.kubernetes.enabled .Values.sandbox.kubernetes.warmPool.enabled }} +{{- if and .Values.sandbox.agentSandbox.enabled .Values.sandbox.agentSandbox.warmPool.enabled }} # Pre-warms N sandbox pods against the shared SandboxTemplate so new claims # bind instantly rather than waiting on image pull + kubelet start. Disabled # by default — enable only after measuring cold-start pain; every pool @@ -16,7 +16,7 @@ metadata: app.kubernetes.io/managed-by: {{ .Release.Service }} helm.sh/chart: {{ include "chart-deco-studio.chart" . }} spec: - replicas: {{ .Values.sandbox.kubernetes.warmPool.size }} + replicas: {{ .Values.sandbox.agentSandbox.warmPool.size }} sandboxTemplateRef: name: studio-sandbox {{- end }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index d500790978..d6caa55619 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -319,7 +319,7 @@ opentelemetry-collector: processors: [memory_limiter, batch] exporters: [debug] -# K8s sandbox runner. Requires MESH_SANDBOX_RUNNER=kubernetes in mesh env. +# Agent-sandbox runner. Requires STUDIO_SANDBOX_RUNNER=agent-sandbox in mesh env. # Disabled by default — self-hosters on the Docker runner (simpler, no # cluster needed) leave this off and don't pay the agent-sandbox operator # install cost. @@ -332,7 +332,7 @@ opentelemetry-collector: # # See deploy/helm/README.md for the enable/upgrade procedure. sandbox: - kubernetes: + agentSandbox: enabled: false image: repository: ghcr.io/decocms/mesh-sandbox diff --git a/deploy/k8s-sandbox/local/README.md b/deploy/k8s-sandbox/local/README.md index b6bc3eb7a6..53f45cbc76 100644 --- a/deploy/k8s-sandbox/local/README.md +++ b/deploy/k8s-sandbox/local/README.md @@ -1,6 +1,6 @@ # Local k8s sandbox (kind) -Scripted local bring-up for `KubernetesSandboxRunner`. One-command cluster + +Scripted local bring-up for `AgentSandboxRunner`. One-command cluster + agent-sandbox operator + mesh `SandboxTemplate`, loaded with the same sandbox image the Docker runner uses. @@ -95,7 +95,7 @@ kubelet (cAdvisor) ──► OTel collector daemonset ``` Pod labels come from `SandboxClaim.spec.additionalPodMetadata.labels`, -populated in `KubernetesSandboxRunner.provision()` from the `tenant` field +populated in `AgentSandboxRunner.provision()` from the `tenant` field on `EnsureOptions`. Verify they're landing: ```bash @@ -120,7 +120,7 @@ k8sattributes filter, ServiceMonitor for the in-cluster mesh Deployment). ## Smoke test Stage 1 exit criterion from PLAN-K8S-MVP.md. Exercises -`KubernetesSandboxRunner` end-to-end against the live kind cluster: +`AgentSandboxRunner` end-to-end against the live kind cluster: ensure → exec → preview fetch → delete → recreate → ensure (warm) → alive → delete. diff --git a/deploy/k8s-sandbox/local/smoke.ts b/deploy/k8s-sandbox/local/smoke.ts index 427dcf3770..eb362a3604 100644 --- a/deploy/k8s-sandbox/local/smoke.ts +++ b/deploy/k8s-sandbox/local/smoke.ts @@ -1,7 +1,7 @@ /** * Stage 1 exit-criterion smoke test (see PLAN-K8S-MVP.md § 1.5/1.6). * - * Runs KubernetesSandboxRunner end-to-end against a live kind cluster: + * Runs AgentSandboxRunner end-to-end against a live kind cluster: * ensure → exec → preview fetch → delete → recreate (cold) → ensure (warm) * → alive → delete. * @@ -28,8 +28,8 @@ // naturally. import { KubeConfig, - KubernetesSandboxRunner, -} from "../../../packages/sandbox/server/runner/k8s"; + AgentSandboxRunner, +} from "../../../packages/sandbox/server/runner/agent-sandbox"; import type { SandboxId } from "../../../packages/sandbox/server/runner/types"; const KIND_CONTEXT = "kind-studio-sandbox-dev"; @@ -72,11 +72,11 @@ async function run(): Promise { // Two runner instances with disjoint in-process maps — simulates a mesh // restart between steps (1st provision → 2nd ensure after delete). // No stateStore: the K8s API is the source of truth for the smoke test. - const runnerA = new KubernetesSandboxRunner({ + const runnerA = new AgentSandboxRunner({ kubeConfig, namespace: NAMESPACE, }); - const runnerB = new KubernetesSandboxRunner({ + const runnerB = new AgentSandboxRunner({ kubeConfig, namespace: NAMESPACE, }); diff --git a/deploy/k8s-sandbox/local/up.sh b/deploy/k8s-sandbox/local/up.sh index 3819d5a040..59d46f9d5a 100755 --- a/deploy/k8s-sandbox/local/up.sh +++ b/deploy/k8s-sandbox/local/up.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Bring up the local kind cluster used by KubernetesSandboxRunner. +# Bring up the local kind cluster used by AgentSandboxRunner. # # Idempotent: re-running re-applies the operator + template and reloads the # sandbox image. Cluster creation is skipped if studio-sandbox-dev already diff --git a/deploy/k8s-sandbox/monitoring/README.md b/deploy/k8s-sandbox/monitoring/README.md index d8a2119c03..d22fe7ec7d 100644 --- a/deploy/k8s-sandbox/monitoring/README.md +++ b/deploy/k8s-sandbox/monitoring/README.md @@ -13,7 +13,7 @@ kubelet (cAdvisor) ──► OTel collector daemonset PodMonitor → kube-prometheus-stack Prometheus → Grafana ``` -Pod labels are populated by `KubernetesSandboxRunner.provision()` from the +Pod labels are populated by `AgentSandboxRunner.provision()` from the `tenant` field on `EnsureOptions` and surface on every sandbox pod via `SandboxClaim.spec.additionalPodMetadata.labels`. diff --git a/packages/mesh-sdk/src/types/virtual-mcp.ts b/packages/mesh-sdk/src/types/virtual-mcp.ts index bf0133220a..693b224d26 100644 --- a/packages/mesh-sdk/src/types/virtual-mcp.ts +++ b/packages/mesh-sdk/src/types/virtual-mcp.ts @@ -137,7 +137,7 @@ export type GithubRepo = z.infer; * `runnerKind` lets the UI construct daemon URLs correctly: * - docker: daemon is reached via the mesh proxy at `/api/sandbox//_daemon/*` * - freestyle: daemon lives at `${previewUrl}/_decopilot_vm/*` on the VM domain - * - kubernetes: daemon is reached via the mesh proxy (same transport as docker); + * - agent-sandbox: daemon is reached via the mesh proxy (same transport as docker); * preview URL is the per-claim HTTPRoute host (in-cluster) or a local port-forward (kind dev). * * `previewUrl` is nullable: blank / tool sandboxes (no `workload`, no dev @@ -154,7 +154,7 @@ export const VmMapEntrySchema = z.object({ .describe( "URL where the VM's iframe-proxied UI is served, or null when the sandbox has no dev server (blank / tool sandboxes).", ), - runnerKind: z.enum(["docker", "freestyle", "kubernetes"]).optional(), + runnerKind: z.enum(["docker", "freestyle", "agent-sandbox"]).optional(), createdAt: z .number() .optional() diff --git a/packages/sandbox/README.md b/packages/sandbox/README.md index 4a237c06cf..d6cc78d7ee 100644 --- a/packages/sandbox/README.md +++ b/packages/sandbox/README.md @@ -19,8 +19,8 @@ Three runner backends live behind the common `SandboxRunner` interface Freestyle-provided HTTPS domain; daemon traffic is base64-wrapped to clear Cloudflare WAF. SDKs are `optionalDependencies` and only pulled in when this runner is selected. -- **Kubernetes** (`./runner/k8s`) — one `SandboxClaim` per sandbox against the - [kubernetes-sigs/agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) +- **agent-sandbox** (`./runner/agent-sandbox`) — one `SandboxClaim` per sandbox + against the [kubernetes-sigs/agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) operator. Mesh talks to pods via apiserver port-forward in dev; in prod, `previewUrlPattern` switches the preview URL to real ingress and skips the dev forward. @@ -30,12 +30,12 @@ Three runner backends live behind the common `SandboxRunner` interface The host app calls `resolveRunnerKindFromEnv()` / `tryResolveRunnerKindFromEnv()` from `./runner`: -1. `MESH_SANDBOX_RUNNER=docker|freestyle|kubernetes` wins when set. +1. `STUDIO_SANDBOX_RUNNER=docker|freestyle|agent-sandbox` wins when set. 2. Otherwise, `FREESTYLE_API_KEY` present → `freestyle`. 3. Otherwise, in `NODE_ENV=production` → unresolved (strict variant throws). 4. Otherwise (dev) → `docker` if the CLI is on `PATH`, else unresolved. -Kubernetes is **explicit-only** — it's never auto-selected, so docker-only +agent-sandbox is **explicit-only** — it's never auto-selected, so docker-only deploys don't accidentally need a kubeconfig. ## URL shape @@ -65,10 +65,10 @@ for this, you can remove them — they're no longer needed. ## Environment -- `MESH_SANDBOX_RUNNER` — pin the runner: `docker`, `freestyle`, or - `kubernetes`. Leave unset in dev to let auto-detect pick docker. +- `STUDIO_SANDBOX_RUNNER` — pin the runner: `docker`, `freestyle`, or + `agent-sandbox`. Leave unset in dev to let auto-detect pick docker. - `FREESTYLE_API_KEY` — required for the Freestyle runner. Presence also - auto-selects it when `MESH_SANDBOX_RUNNER` is unset. + auto-selects it when `STUDIO_SANDBOX_RUNNER` is unset. - `MESH_SANDBOX_IMAGE` — override the Docker runner image (default `mesh-sandbox:local`, built from `image/Dockerfile`). - `SANDBOX_INGRESS_PORT` (default `7070`) — local Docker ingress bind port. diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index da613e79fb..ae0376ce98 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -12,7 +12,7 @@ "exports": { "./shared": "./shared.ts", "./runner": "./server/runner/index.ts", - "./runner/k8s": "./server/runner/k8s/index.ts", + "./runner/agent-sandbox": "./server/runner/agent-sandbox/index.ts", "./runner/freestyle": "./server/runner/freestyle/index.ts" }, "dependencies": { diff --git a/packages/sandbox/server/runner/k8s/client.test.ts b/packages/sandbox/server/runner/agent-sandbox/client.test.ts similarity index 100% rename from packages/sandbox/server/runner/k8s/client.test.ts rename to packages/sandbox/server/runner/agent-sandbox/client.test.ts diff --git a/packages/sandbox/server/runner/k8s/client.ts b/packages/sandbox/server/runner/agent-sandbox/client.ts similarity index 100% rename from packages/sandbox/server/runner/k8s/client.ts rename to packages/sandbox/server/runner/agent-sandbox/client.ts diff --git a/packages/sandbox/server/runner/k8s/constants.ts b/packages/sandbox/server/runner/agent-sandbox/constants.ts similarity index 100% rename from packages/sandbox/server/runner/k8s/constants.ts rename to packages/sandbox/server/runner/agent-sandbox/constants.ts diff --git a/packages/sandbox/server/runner/k8s/index.ts b/packages/sandbox/server/runner/agent-sandbox/index.ts similarity index 81% rename from packages/sandbox/server/runner/k8s/index.ts rename to packages/sandbox/server/runner/agent-sandbox/index.ts index f4d0253b06..711ee05b3e 100644 --- a/packages/sandbox/server/runner/k8s/index.ts +++ b/packages/sandbox/server/runner/agent-sandbox/index.ts @@ -15,5 +15,5 @@ export type { SandboxResource, WaitForSandboxReadyResult, } from "./client"; -export { KubernetesSandboxRunner, HANDLE_PREFIX } from "./runner"; -export type { KubernetesRunnerOptions } from "./runner"; +export { AgentSandboxRunner, HANDLE_PREFIX } from "./runner"; +export type { AgentSandboxRunnerOptions } from "./runner"; diff --git a/packages/sandbox/server/runner/k8s/runner.ts b/packages/sandbox/server/runner/agent-sandbox/runner.ts similarity index 99% rename from packages/sandbox/server/runner/k8s/runner.ts rename to packages/sandbox/server/runner/agent-sandbox/runner.ts index b143fbb5de..6ed67a069f 100644 --- a/packages/sandbox/server/runner/k8s/runner.ts +++ b/packages/sandbox/server/runner/agent-sandbox/runner.ts @@ -1,5 +1,5 @@ /** - * Kubernetes sandbox runner. + * Agent-sandbox runner. * * Provisions one SandboxClaim per (user, projectRef) against the * kubernetes-sigs/agent-sandbox operator. Mesh runs outside the cluster @@ -71,8 +71,8 @@ import { } from "./client"; import { K8S_CONSTANTS } from "./constants"; -const RUNNER_KIND = "kubernetes" as const; -const LOG_LABEL = "KubernetesSandboxRunner"; +const RUNNER_KIND = "agent-sandbox" as const; +const LOG_LABEL = "AgentSandboxRunner"; // Shared-namespace topology for MVP; tenancy enforced by unguessable claim // names (sha256(userId:projectRef)). Per-org namespaces are deferred. @@ -234,7 +234,7 @@ interface PersistedK8sState { [k: string]: unknown; } -export interface KubernetesRunnerOptions { +export interface AgentSandboxRunnerOptions { stateStore?: RunnerStateStore; previewUrlPattern?: string; /** Defaults to `new KubeConfig().loadFromDefault()`. Tests pass a stub. */ @@ -263,7 +263,7 @@ export interface KubernetesRunnerOptions { meter?: Meter; } -export class KubernetesSandboxRunner implements SandboxRunner { +export class AgentSandboxRunner implements SandboxRunner { readonly kind = RUNNER_KIND; private readonly records = new Map(); @@ -283,7 +283,7 @@ export class KubernetesSandboxRunner implements SandboxRunner { */ private readonly metrics: RunnerMetrics | null; - constructor(opts: KubernetesRunnerOptions = {}) { + constructor(opts: AgentSandboxRunnerOptions = {}) { this.stateStore = opts.stateStore ?? null; this.previewUrlPattern = opts.previewUrlPattern ?? null; this.kubeConfig = opts.kubeConfig ?? loadDefaultKubeConfig(); diff --git a/packages/sandbox/server/runner/index.ts b/packages/sandbox/server/runner/index.ts index 682c29069b..f822178b37 100644 --- a/packages/sandbox/server/runner/index.ts +++ b/packages/sandbox/server/runner/index.ts @@ -1,8 +1,8 @@ /** * Public surface. Ships `DockerSandboxRunner` only via the default entry; - * Freestyle and Kubernetes sit behind their own subpath exports (./runner/ - * freestyle, ./runner/k8s) because their SDKs are heavy and not every deploy - * needs them. + * Freestyle and agent-sandbox sit behind their own subpath exports (./runner/ + * freestyle, ./runner/agent-sandbox) because their SDKs are heavy and not + * every deploy needs them. */ import { spawnSync } from "node:child_process"; @@ -80,22 +80,22 @@ function isDockerInstalled(): boolean { /** * Rules: - * 1. `MESH_SANDBOX_RUNNER=docker|freestyle|kubernetes` — honored. + * 1. `STUDIO_SANDBOX_RUNNER=docker|freestyle|agent-sandbox` — honored. * 2. No explicit value, `FREESTYLE_API_KEY` set — pick freestyle. * 3. Production w/o explicit value and no freestyle key — null. * 4. Dev w/o explicit value — docker if CLI present, else null. * - * Kubernetes is explicit-only: never auto-selected — callers must opt in - * with `MESH_SANDBOX_RUNNER=kubernetes` so docker-only dev stays the default. + * agent-sandbox is explicit-only: never auto-selected — callers must opt in + * with `STUDIO_SANDBOX_RUNNER=agent-sandbox` so docker-only dev stays the default. */ export function tryResolveRunnerKindFromEnv(): RunnerKind | null { - const raw = process.env.MESH_SANDBOX_RUNNER; - if (raw === "docker" || raw === "freestyle" || raw === "kubernetes") { + const raw = process.env.STUDIO_SANDBOX_RUNNER; + if (raw === "docker" || raw === "freestyle" || raw === "agent-sandbox") { return raw; } if (raw && raw.length > 0) { throw new Error( - `Unknown MESH_SANDBOX_RUNNER="${raw}" — expected "docker", "freestyle", or "kubernetes".`, + `Unknown STUDIO_SANDBOX_RUNNER="${raw}" — expected "docker", "freestyle", or "agent-sandbox".`, ); } if (process.env.FREESTYLE_API_KEY) return "freestyle"; @@ -109,12 +109,12 @@ export function resolveRunnerKindFromEnv(): RunnerKind { if (kind) return kind; if (process.env.NODE_ENV === "production") { throw new Error( - `MESH_SANDBOX_RUNNER must be set explicitly in production — ` + - `choose "docker", "freestyle", or "kubernetes" (or set FREESTYLE_API_KEY).`, + `STUDIO_SANDBOX_RUNNER must be set explicitly in production — ` + + `choose "docker", "freestyle", or "agent-sandbox" (or set FREESTYLE_API_KEY).`, ); } throw new Error( `No sandbox runner available: Docker CLI not found on PATH. ` + - `Install Docker for local dev, or set MESH_SANDBOX_RUNNER explicitly.`, + `Install Docker for local dev, or set STUDIO_SANDBOX_RUNNER explicitly.`, ); } diff --git a/packages/sandbox/server/runner/types.ts b/packages/sandbox/server/runner/types.ts index d02cb4d6aa..4b52e847dd 100644 --- a/packages/sandbox/server/runner/types.ts +++ b/packages/sandbox/server/runner/types.ts @@ -86,7 +86,7 @@ export interface ProxyRequestInit { * Persisted on `vmMap` and `sandbox_runner_state.runner_kind`. When widening, * keep `VmMapEntry.runnerKind` in sync. */ -export type RunnerKind = "docker" | "freestyle" | "kubernetes"; +export type RunnerKind = "docker" | "freestyle" | "agent-sandbox"; export interface SandboxRunner { readonly kind: RunnerKind;