@wenathlan/saddle 1.8.17 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +1339 -0
- package/README.md +68 -22
- package/alternatives.ts +1218 -0
- package/biome.json +153 -0
- package/boards.json +1517 -0
- package/compute.ts +3144 -0
- package/cores.json +1801 -0
- package/dist/acquisition.d.ts +359 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +629 -0
- package/dist/acquisition.js.map +1 -0
- package/dist/alternatives.d.ts +509 -0
- package/dist/alternatives.d.ts.map +1 -0
- package/dist/alternatives.js +951 -0
- package/dist/alternatives.js.map +1 -0
- package/dist/automation.d.ts +426 -0
- package/dist/automation.d.ts.map +1 -0
- package/dist/automation.js +561 -0
- package/dist/automation.js.map +1 -0
- package/dist/browser.d.ts +562 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +1057 -0
- package/dist/browser.js.map +1 -0
- package/dist/capacitor.config.d.ts +13 -0
- package/dist/capacitor.config.d.ts.map +1 -0
- package/dist/capacitor.config.js +26 -0
- package/dist/capacitor.config.js.map +1 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +111 -0
- package/dist/cli.js.map +1 -0
- package/dist/communication.d.ts +192 -0
- package/dist/communication.d.ts.map +1 -0
- package/dist/communication.js +418 -0
- package/dist/communication.js.map +1 -0
- package/dist/compute.d.ts +1161 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +2273 -0
- package/dist/compute.js.map +1 -0
- package/dist/distribution.d.ts +529 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +811 -0
- package/dist/distribution.js.map +1 -0
- package/dist/execution.d.ts +530 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +761 -0
- package/dist/execution.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +60 -0
- package/dist/format.js.map +1 -0
- package/dist/foundation.d.ts +176 -0
- package/dist/foundation.d.ts.map +1 -0
- package/dist/foundation.js +309 -0
- package/dist/foundation.js.map +1 -0
- package/dist/index.d.ts +733 -122
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1018 -122
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +236 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +236 -0
- package/dist/integration.js.map +1 -0
- package/dist/intelligence.d.ts +94 -0
- package/dist/intelligence.d.ts.map +1 -0
- package/dist/intelligence.js +145 -0
- package/dist/intelligence.js.map +1 -0
- package/dist/isolation.d.ts +129 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +96 -0
- package/dist/isolation.js.map +1 -0
- package/dist/media.d.ts +344 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +986 -0
- package/dist/media.js.map +1 -0
- package/dist/modes.d.ts +359 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +134 -0
- package/dist/modes.js.map +1 -0
- package/dist/{library/public.d.ts → operations.d.ts} +15 -1
- package/dist/operations.d.ts.map +1 -0
- package/dist/{library/public.js → operations.js} +18 -7
- package/dist/operations.js.map +1 -0
- package/dist/orchestrator.d.ts +1339 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +4308 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/performance.d.ts +416 -0
- package/dist/performance.d.ts.map +1 -0
- package/dist/performance.js +827 -0
- package/dist/performance.js.map +1 -0
- package/dist/quantum.d.ts +810 -0
- package/dist/quantum.d.ts.map +1 -0
- package/dist/quantum.js +2073 -0
- package/dist/quantum.js.map +1 -0
- package/dist/render.d.ts +381 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1073 -0
- package/dist/render.js.map +1 -0
- package/dist/scheduler.d.ts +596 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +1259 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/security.d.ts +501 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +1170 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/{server/node.js → server.js} +16 -2
- package/dist/server.js.map +1 -0
- package/dist/tiers.d.ts +1002 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +2165 -0
- package/dist/tiers.js.map +1 -0
- package/dist/virtual.d.ts +641 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +1285 -0
- package/dist/virtual.js.map +1 -0
- package/dist/virtualcpu.d.ts +316 -0
- package/dist/virtualcpu.d.ts.map +1 -0
- package/dist/virtualcpu.js +1180 -0
- package/dist/virtualcpu.js.map +1 -0
- package/dist/virtualgpu.d.ts +345 -0
- package/dist/virtualgpu.d.ts.map +1 -0
- package/dist/virtualgpu.js +1000 -0
- package/dist/virtualgpu.js.map +1 -0
- package/dist/virtualization.d.ts +607 -0
- package/dist/virtualization.d.ts.map +1 -0
- package/dist/virtualization.js +1187 -0
- package/dist/virtualization.js.map +1 -0
- package/dist/virtualmemory.d.ts +818 -0
- package/dist/virtualmemory.d.ts.map +1 -0
- package/dist/virtualmemory.js +1796 -0
- package/dist/virtualmemory.js.map +1 -0
- package/dist/webscrape.d.ts +720 -0
- package/dist/webscrape.d.ts.map +1 -0
- package/dist/webscrape.js +2697 -0
- package/dist/webscrape.js.map +1 -0
- package/docker.config +769 -0
- package/docs/CONVERSA.txt +1523 -0
- package/docs/alternatives.md +499 -0
- package/docs/architecture-1.8.19.md +76 -0
- package/docs/architecture-1.8.19.mmd +33 -0
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/architecture.md +355 -0
- package/docs/artifactavailability.md +6 -0
- package/docs/brancharchive-2026-08-18.md +18 -0
- package/docs/browser.md +7 -0
- package/docs/consolidation.md +70 -0
- package/docs/e2ugh-engine.md +528 -0
- package/{dist/examples/localjob.js → docs/example-localjob.ts} +3 -3
- package/{dist/examples/publicapi.js → docs/example-publicapi.ts} +2 -2
- package/docs/hardware.md +342 -0
- package/docs/optimization.md +126 -0
- package/docs/performance.md +499 -0
- package/docs/planning.1.8.18.md +94 -0
- package/docs/planning.1.8.19.md +49 -0
- package/docs/releasenotes-1.8.17.md +6 -0
- package/docs/releasenotes-1.8.18.md +32 -0
- package/docs/releasenotes-1.8.19.md +35 -0
- package/docs/research-1.8.18-isolation.md +43 -0
- package/docs/research-1.8.19-virtual-browser.md +27 -0
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- package/docs/security.md +454 -0
- package/docs/todo-1.8.16.md +56 -1
- package/docs/todo-1.8.18.md +164 -0
- package/docs/todo-1.8.19.md +241 -0
- package/docs/viability.md +777 -0
- package/docs/virtualization.md +335 -0
- package/docs/web-duplicatecleanup.md +9 -0
- package/docs/web-ideas.md +106 -0
- package/docs/workflowimprovements-2026-08-19.md +70 -0
- package/gpumonitor.cpp +1543 -0
- package/gpus.json +2408 -0
- package/index.ts +1478 -0
- package/media.ts +1337 -0
- package/mttg.config +1549 -0
- package/orchestrator.ts +5462 -0
- package/package.json +304 -55
- package/passage.config +1331 -0
- package/performance.ts +1049 -0
- package/processors.json +2217 -0
- package/qemu.config +1062 -0
- package/qemubridge.py +1340 -0
- package/quantum.ts +2393 -0
- package/render.ts +1325 -0
- package/scheduler.ts +1600 -0
- package/security.ts +1449 -0
- package/tiers.ts +2871 -0
- package/tsconfig.json +29 -0
- package/virtualcpu.ts +1273 -0
- package/virtualgpu.ts +1181 -0
- package/virtualhardware.c +1011 -0
- package/virtualhardware.json +739 -0
- package/virtualization.ts +1628 -0
- package/virtualizationcore.cpp +4635 -0
- package/virtualmemory.ts +2303 -0
- package/vm.config.json +1910 -0
- package/{extension → web/extension}/manifest.json +1 -1
- package/web/readme.md +382 -0
- package/web/tsconfig.json +25 -0
- package/dist/adapters/forge.d.ts +0 -17
- package/dist/adapters/forge.d.ts.map +0 -1
- package/dist/adapters/forge.js +0 -19
- package/dist/adapters/forge.js.map +0 -1
- package/dist/adapters/forgejo.d.ts +0 -49
- package/dist/adapters/forgejo.d.ts.map +0 -1
- package/dist/adapters/forgejo.js +0 -8
- package/dist/adapters/forgejo.js.map +0 -1
- package/dist/adapters/github.d.ts +0 -12
- package/dist/adapters/github.d.ts.map +0 -1
- package/dist/adapters/github.js +0 -20
- package/dist/adapters/github.js.map +0 -1
- package/dist/adapters/gitlab.d.ts +0 -17
- package/dist/adapters/gitlab.d.ts.map +0 -1
- package/dist/adapters/gitlab.js +0 -10
- package/dist/adapters/gitlab.js.map +0 -1
- package/dist/adapters/huggingface.d.ts +0 -17
- package/dist/adapters/huggingface.d.ts.map +0 -1
- package/dist/adapters/huggingface.js +0 -6
- package/dist/adapters/huggingface.js.map +0 -1
- package/dist/adapters/socket.d.ts +0 -11
- package/dist/adapters/socket.d.ts.map +0 -1
- package/dist/adapters/socket.js +0 -17
- package/dist/adapters/socket.js.map +0 -1
- package/dist/adapters/transport.d.ts +0 -7
- package/dist/adapters/transport.d.ts.map +0 -1
- package/dist/adapters/transport.js +0 -36
- package/dist/adapters/transport.js.map +0 -1
- package/dist/ai/chunk.d.ts +0 -2
- package/dist/ai/chunk.d.ts.map +0 -1
- package/dist/ai/chunk.js +0 -36
- package/dist/ai/chunk.js.map +0 -1
- package/dist/ai/llmstxt.d.ts +0 -6
- package/dist/ai/llmstxt.d.ts.map +0 -1
- package/dist/ai/llmstxt.js +0 -12
- package/dist/ai/llmstxt.js.map +0 -1
- package/dist/ai/provenance.d.ts +0 -22
- package/dist/ai/provenance.d.ts.map +0 -1
- package/dist/ai/provenance.js +0 -25
- package/dist/ai/provenance.js.map +0 -1
- package/dist/ai/rag.d.ts +0 -26
- package/dist/ai/rag.d.ts.map +0 -1
- package/dist/ai/rag.js +0 -22
- package/dist/ai/rag.js.map +0 -1
- package/dist/ai/tokens.d.ts +0 -15
- package/dist/ai/tokens.d.ts.map +0 -1
- package/dist/ai/tokens.js +0 -9
- package/dist/ai/tokens.js.map +0 -1
- package/dist/api/auth.d.ts +0 -16
- package/dist/api/auth.d.ts.map +0 -1
- package/dist/api/auth.js +0 -19
- package/dist/api/auth.js.map +0 -1
- package/dist/api/contracts.d.ts +0 -28
- package/dist/api/contracts.d.ts.map +0 -1
- package/dist/api/contracts.js +0 -14
- package/dist/api/contracts.js.map +0 -1
- package/dist/api/control.d.ts +0 -10
- package/dist/api/control.d.ts.map +0 -1
- package/dist/api/control.js +0 -35
- package/dist/api/control.js.map +0 -1
- package/dist/api/http.d.ts +0 -7
- package/dist/api/http.d.ts.map +0 -1
- package/dist/api/http.js +0 -12
- package/dist/api/http.js.map +0 -1
- package/dist/api/rate.d.ts +0 -28
- package/dist/api/rate.d.ts.map +0 -1
- package/dist/api/rate.js +0 -36
- package/dist/api/rate.js.map +0 -1
- package/dist/api/security.d.ts +0 -12
- package/dist/api/security.d.ts.map +0 -1
- package/dist/api/security.js +0 -56
- package/dist/api/security.js.map +0 -1
- package/dist/api/service.d.ts +0 -5
- package/dist/api/service.d.ts.map +0 -1
- package/dist/api/service.js +0 -70
- package/dist/api/service.js.map +0 -1
- package/dist/apps/registry.d.ts +0 -33
- package/dist/apps/registry.d.ts.map +0 -1
- package/dist/apps/registry.js +0 -24
- package/dist/apps/registry.js.map +0 -1
- package/dist/binary/archive.d.ts +0 -27
- package/dist/binary/archive.d.ts.map +0 -1
- package/dist/binary/archive.js +0 -46
- package/dist/binary/archive.js.map +0 -1
- package/dist/binary/build.d.ts +0 -29
- package/dist/binary/build.d.ts.map +0 -1
- package/dist/binary/build.js +0 -17
- package/dist/binary/build.js.map +0 -1
- package/dist/binary/transform.d.ts +0 -70
- package/dist/binary/transform.d.ts.map +0 -1
- package/dist/binary/transform.js +0 -105
- package/dist/binary/transform.js.map +0 -1
- package/dist/bot/adapter.d.ts +0 -5
- package/dist/bot/adapter.d.ts.map +0 -1
- package/dist/bot/adapter.js +0 -11
- package/dist/bot/adapter.js.map +0 -1
- package/dist/bot/bot.d.ts +0 -28
- package/dist/bot/bot.d.ts.map +0 -1
- package/dist/bot/bot.js +0 -52
- package/dist/bot/bot.js.map +0 -1
- package/dist/bot/commands.d.ts +0 -8
- package/dist/bot/commands.d.ts.map +0 -1
- package/dist/bot/commands.js +0 -19
- package/dist/bot/commands.js.map +0 -1
- package/dist/bot/permissions.d.ts +0 -13
- package/dist/bot/permissions.d.ts.map +0 -1
- package/dist/bot/permissions.js +0 -16
- package/dist/bot/permissions.js.map +0 -1
- package/dist/browser/actions.d.ts +0 -33
- package/dist/browser/actions.d.ts.map +0 -1
- package/dist/browser/actions.js +0 -44
- package/dist/browser/actions.js.map +0 -1
- package/dist/browser/agent.d.ts +0 -21
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js +0 -13
- package/dist/browser/agent.js.map +0 -1
- package/dist/browser/context.d.ts +0 -43
- package/dist/browser/context.d.ts.map +0 -1
- package/dist/browser/context.js +0 -54
- package/dist/browser/context.js.map +0 -1
- package/dist/browser/fingerprint.d.ts +0 -4
- package/dist/browser/fingerprint.d.ts.map +0 -1
- package/dist/browser/fingerprint.js +0 -12
- package/dist/browser/fingerprint.js.map +0 -1
- package/dist/browser/index.d.ts +0 -11
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -11
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/playwright.d.ts +0 -11
- package/dist/browser/playwright.d.ts.map +0 -1
- package/dist/browser/playwright.js +0 -24
- package/dist/browser/playwright.js.map +0 -1
- package/dist/browser/recorder.d.ts +0 -21
- package/dist/browser/recorder.d.ts.map +0 -1
- package/dist/browser/recorder.js +0 -58
- package/dist/browser/recorder.js.map +0 -1
- package/dist/browser/session.d.ts +0 -14
- package/dist/browser/session.d.ts.map +0 -1
- package/dist/browser/session.js +0 -21
- package/dist/browser/session.js.map +0 -1
- package/dist/browser/snapshot.d.ts +0 -69
- package/dist/browser/snapshot.d.ts.map +0 -1
- package/dist/browser/snapshot.js +0 -133
- package/dist/browser/snapshot.js.map +0 -1
- package/dist/captcha/contract.d.ts +0 -35
- package/dist/captcha/contract.d.ts.map +0 -1
- package/dist/captcha/contract.js +0 -17
- package/dist/captcha/contract.js.map +0 -1
- package/dist/captcha/evidence.d.ts +0 -10
- package/dist/captcha/evidence.d.ts.map +0 -1
- package/dist/captcha/evidence.js +0 -9
- package/dist/captcha/evidence.js.map +0 -1
- package/dist/captcha/guard.d.ts +0 -23
- package/dist/captcha/guard.d.ts.map +0 -1
- package/dist/captcha/guard.js +0 -12
- package/dist/captcha/guard.js.map +0 -1
- package/dist/cli/main.d.ts +0 -4
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -46
- package/dist/cli/main.js.map +0 -1
- package/dist/core/errors.d.ts +0 -84
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/errors.js +0 -54
- package/dist/core/errors.js.map +0 -1
- package/dist/core/events.d.ts +0 -9
- package/dist/core/events.d.ts.map +0 -1
- package/dist/core/events.js +0 -21
- package/dist/core/events.js.map +0 -1
- package/dist/core/hash.d.ts +0 -10
- package/dist/core/hash.d.ts.map +0 -1
- package/dist/core/hash.js +0 -82
- package/dist/core/hash.js.map +0 -1
- package/dist/core/ids.d.ts +0 -10
- package/dist/core/ids.d.ts.map +0 -1
- package/dist/core/ids.js +0 -15
- package/dist/core/ids.js.map +0 -1
- package/dist/delivery/manifest.d.ts +0 -35
- package/dist/delivery/manifest.d.ts.map +0 -1
- package/dist/delivery/manifest.js +0 -68
- package/dist/delivery/manifest.js.map +0 -1
- package/dist/deploy/index.d.ts +0 -6
- package/dist/deploy/index.d.ts.map +0 -1
- package/dist/deploy/index.js +0 -6
- package/dist/deploy/index.js.map +0 -1
- package/dist/dispatch/resumable.d.ts +0 -102
- package/dist/dispatch/resumable.d.ts.map +0 -1
- package/dist/dispatch/resumable.js +0 -64
- package/dist/dispatch/resumable.js.map +0 -1
- package/dist/dispatch/workflow.d.ts +0 -6
- package/dist/dispatch/workflow.d.ts.map +0 -1
- package/dist/dispatch/workflow.js +0 -37
- package/dist/dispatch/workflow.js.map +0 -1
- package/dist/domain/artifacts.d.ts +0 -12
- package/dist/domain/artifacts.d.ts.map +0 -1
- package/dist/domain/artifacts.js +0 -14
- package/dist/domain/artifacts.js.map +0 -1
- package/dist/domain/jobs.d.ts +0 -12
- package/dist/domain/jobs.d.ts.map +0 -1
- package/dist/domain/jobs.js +0 -20
- package/dist/domain/jobs.js.map +0 -1
- package/dist/domain/providers.d.ts +0 -10
- package/dist/domain/providers.d.ts.map +0 -1
- package/dist/domain/providers.js +0 -8
- package/dist/domain/providers.js.map +0 -1
- package/dist/domain/runtime.d.ts +0 -14
- package/dist/domain/runtime.d.ts.map +0 -1
- package/dist/domain/runtime.js +0 -10
- package/dist/domain/runtime.js.map +0 -1
- package/dist/domain/sessions.d.ts +0 -12
- package/dist/domain/sessions.d.ts.map +0 -1
- package/dist/domain/sessions.js +0 -57
- package/dist/domain/sessions.js.map +0 -1
- package/dist/examples/localjob.d.ts +0 -2
- package/dist/examples/localjob.d.ts.map +0 -1
- package/dist/examples/localjob.js.map +0 -1
- package/dist/examples/publicapi.d.ts +0 -2
- package/dist/examples/publicapi.d.ts.map +0 -1
- package/dist/examples/publicapi.js.map +0 -1
- package/dist/extension/build.d.ts +0 -9
- package/dist/extension/build.d.ts.map +0 -1
- package/dist/extension/build.js +0 -85
- package/dist/extension/build.js.map +0 -1
- package/dist/extension/content.d.ts +0 -6
- package/dist/extension/content.d.ts.map +0 -1
- package/dist/extension/content.js +0 -135
- package/dist/extension/content.js.map +0 -1
- package/dist/extension/index.d.ts +0 -7
- package/dist/extension/index.d.ts.map +0 -1
- package/dist/extension/index.js +0 -7
- package/dist/extension/index.js.map +0 -1
- package/dist/extension/pagebridge.d.ts +0 -6
- package/dist/extension/pagebridge.d.ts.map +0 -1
- package/dist/extension/pagebridge.js +0 -42
- package/dist/extension/pagebridge.js.map +0 -1
- package/dist/extension/permissions.d.ts +0 -26
- package/dist/extension/permissions.d.ts.map +0 -1
- package/dist/extension/permissions.js +0 -28
- package/dist/extension/permissions.js.map +0 -1
- package/dist/extension/popup.d.ts +0 -5
- package/dist/extension/popup.d.ts.map +0 -1
- package/dist/extension/popup.js +0 -26
- package/dist/extension/popup.js.map +0 -1
- package/dist/extension/protocol.d.ts +0 -58
- package/dist/extension/protocol.d.ts.map +0 -1
- package/dist/extension/protocol.js +0 -101
- package/dist/extension/protocol.js.map +0 -1
- package/dist/extension/serviceworker.d.ts +0 -25
- package/dist/extension/serviceworker.d.ts.map +0 -1
- package/dist/extension/serviceworker.js +0 -102
- package/dist/extension/serviceworker.js.map +0 -1
- package/dist/extension/worker.d.ts +0 -28
- package/dist/extension/worker.d.ts.map +0 -1
- package/dist/extension/worker.js +0 -22
- package/dist/extension/worker.js.map +0 -1
- package/dist/format/check.d.ts +0 -8
- package/dist/format/check.d.ts.map +0 -1
- package/dist/format/check.js +0 -38
- package/dist/format/check.js.map +0 -1
- package/dist/library/public.d.ts.map +0 -1
- package/dist/library/public.js.map +0 -1
- package/dist/mcp/browser.d.ts +0 -9
- package/dist/mcp/browser.d.ts.map +0 -1
- package/dist/mcp/browser.js +0 -13
- package/dist/mcp/browser.js.map +0 -1
- package/dist/mcp/server.d.ts +0 -35
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -40
- package/dist/mcp/server.js.map +0 -1
- package/dist/mcp/transport.d.ts +0 -7
- package/dist/mcp/transport.d.ts.map +0 -1
- package/dist/mcp/transport.js +0 -17
- package/dist/mcp/transport.js.map +0 -1
- package/dist/memory/bridge.d.ts +0 -14
- package/dist/memory/bridge.d.ts.map +0 -1
- package/dist/memory/bridge.js +0 -16
- package/dist/memory/bridge.js.map +0 -1
- package/dist/memory/engine.d.ts +0 -38
- package/dist/memory/engine.d.ts.map +0 -1
- package/dist/memory/engine.js +0 -109
- package/dist/memory/engine.js.map +0 -1
- package/dist/memory/modes.d.ts +0 -43
- package/dist/memory/modes.d.ts.map +0 -1
- package/dist/memory/modes.js +0 -66
- package/dist/memory/modes.js.map +0 -1
- package/dist/memory/objects.d.ts +0 -21
- package/dist/memory/objects.d.ts.map +0 -1
- package/dist/memory/objects.js +0 -20
- package/dist/memory/objects.js.map +0 -1
- package/dist/memory/planner.d.ts +0 -66
- package/dist/memory/planner.d.ts.map +0 -1
- package/dist/memory/planner.js +0 -108
- package/dist/memory/planner.js.map +0 -1
- package/dist/memory/targets.d.ts +0 -78
- package/dist/memory/targets.d.ts.map +0 -1
- package/dist/memory/targets.js +0 -29
- package/dist/memory/targets.js.map +0 -1
- package/dist/memory/transforms.d.ts +0 -19
- package/dist/memory/transforms.d.ts.map +0 -1
- package/dist/memory/transforms.js +0 -14
- package/dist/memory/transforms.js.map +0 -1
- package/dist/modes/matrix.d.ts +0 -23
- package/dist/modes/matrix.d.ts.map +0 -1
- package/dist/modes/matrix.js +0 -18
- package/dist/modes/matrix.js.map +0 -1
- package/dist/modes/modes.d.ts +0 -33
- package/dist/modes/modes.d.ts.map +0 -1
- package/dist/modes/modes.js +0 -15
- package/dist/modes/modes.js.map +0 -1
- package/dist/modes/resolve.d.ts +0 -96
- package/dist/modes/resolve.d.ts.map +0 -1
- package/dist/modes/resolve.js +0 -48
- package/dist/modes/resolve.js.map +0 -1
- package/dist/observability/metrics.d.ts +0 -14
- package/dist/observability/metrics.d.ts.map +0 -1
- package/dist/observability/metrics.js +0 -18
- package/dist/observability/metrics.js.map +0 -1
- package/dist/packager/manifest.d.ts +0 -66
- package/dist/packager/manifest.d.ts.map +0 -1
- package/dist/packager/manifest.js +0 -94
- package/dist/packager/manifest.js.map +0 -1
- package/dist/packager/publish.d.ts +0 -51
- package/dist/packager/publish.d.ts.map +0 -1
- package/dist/packager/publish.js +0 -18
- package/dist/packager/publish.js.map +0 -1
- package/dist/packager/targetcli.d.ts +0 -7
- package/dist/packager/targetcli.d.ts.map +0 -1
- package/dist/packager/targetcli.js +0 -22
- package/dist/packager/targetcli.js.map +0 -1
- package/dist/persistence/adapter.d.ts +0 -5
- package/dist/persistence/adapter.d.ts.map +0 -1
- package/dist/persistence/adapter.js +0 -11
- package/dist/persistence/adapter.js.map +0 -1
- package/dist/persistence/drizzle.d.ts +0 -2
- package/dist/persistence/drizzle.d.ts.map +0 -1
- package/dist/persistence/drizzle.js +0 -12
- package/dist/persistence/drizzle.js.map +0 -1
- package/dist/persistence/memory.d.ts +0 -2
- package/dist/persistence/memory.d.ts.map +0 -1
- package/dist/persistence/memory.js +0 -27
- package/dist/persistence/memory.js.map +0 -1
- package/dist/persistence/migrations.d.ts +0 -19
- package/dist/persistence/migrations.d.ts.map +0 -1
- package/dist/persistence/migrations.js +0 -13
- package/dist/persistence/migrations.js.map +0 -1
- package/dist/persistence/prisma.d.ts +0 -2
- package/dist/persistence/prisma.d.ts.map +0 -1
- package/dist/persistence/prisma.js +0 -25
- package/dist/persistence/prisma.js.map +0 -1
- package/dist/persistence/schema.d.ts +0 -63
- package/dist/persistence/schema.d.ts.map +0 -1
- package/dist/persistence/schema.js +0 -28
- package/dist/persistence/schema.js.map +0 -1
- package/dist/persistence/sql.d.ts +0 -3
- package/dist/persistence/sql.d.ts.map +0 -1
- package/dist/persistence/sql.js +0 -37
- package/dist/persistence/sql.js.map +0 -1
- package/dist/protocol/blocks.d.ts +0 -10
- package/dist/protocol/blocks.d.ts.map +0 -1
- package/dist/protocol/blocks.js +0 -26
- package/dist/protocol/blocks.js.map +0 -1
- package/dist/protocol/json.d.ts +0 -6
- package/dist/protocol/json.d.ts.map +0 -1
- package/dist/protocol/json.js +0 -6
- package/dist/protocol/json.js.map +0 -1
- package/dist/protocol/ndjson.d.ts +0 -3
- package/dist/protocol/ndjson.d.ts.map +0 -1
- package/dist/protocol/ndjson.js +0 -20
- package/dist/protocol/ndjson.js.map +0 -1
- package/dist/protocol/sse.d.ts +0 -7
- package/dist/protocol/sse.d.ts.map +0 -1
- package/dist/protocol/sse.js +0 -25
- package/dist/protocol/sse.js.map +0 -1
- package/dist/proxy/pool.d.ts +0 -9
- package/dist/proxy/pool.d.ts.map +0 -1
- package/dist/proxy/pool.js +0 -27
- package/dist/proxy/pool.js.map +0 -1
- package/dist/queue/idempotency.d.ts +0 -10
- package/dist/queue/idempotency.d.ts.map +0 -1
- package/dist/queue/idempotency.js +0 -13
- package/dist/queue/idempotency.js.map +0 -1
- package/dist/queue/persistent.d.ts +0 -14
- package/dist/queue/persistent.d.ts.map +0 -1
- package/dist/queue/persistent.js +0 -66
- package/dist/queue/persistent.js.map +0 -1
- package/dist/queue/queue.d.ts +0 -7
- package/dist/queue/queue.d.ts.map +0 -1
- package/dist/queue/queue.js +0 -55
- package/dist/queue/queue.js.map +0 -1
- package/dist/queue/saga.d.ts +0 -5
- package/dist/queue/saga.d.ts.map +0 -1
- package/dist/queue/saga.js +0 -20
- package/dist/queue/saga.js.map +0 -1
- package/dist/release/assets.d.ts +0 -144
- package/dist/release/assets.d.ts.map +0 -1
- package/dist/release/assets.js +0 -157
- package/dist/release/assets.js.map +0 -1
- package/dist/release/evidence.d.ts +0 -92
- package/dist/release/evidence.d.ts.map +0 -1
- package/dist/release/evidence.js +0 -148
- package/dist/release/evidence.js.map +0 -1
- package/dist/release/verify.d.ts +0 -12
- package/dist/release/verify.d.ts.map +0 -1
- package/dist/release/verify.js +0 -112
- package/dist/release/verify.js.map +0 -1
- package/dist/runners/chain.d.ts +0 -125
- package/dist/runners/chain.d.ts.map +0 -1
- package/dist/runners/chain.js +0 -95
- package/dist/runners/chain.js.map +0 -1
- package/dist/runners/health.d.ts +0 -36
- package/dist/runners/health.d.ts.map +0 -1
- package/dist/runners/health.js +0 -26
- package/dist/runners/health.js.map +0 -1
- package/dist/runners/heartbeat.d.ts +0 -23
- package/dist/runners/heartbeat.d.ts.map +0 -1
- package/dist/runners/heartbeat.js +0 -29
- package/dist/runners/heartbeat.js.map +0 -1
- package/dist/runners/inprocess.d.ts +0 -17
- package/dist/runners/inprocess.d.ts.map +0 -1
- package/dist/runners/inprocess.js +0 -20
- package/dist/runners/inprocess.js.map +0 -1
- package/dist/runners/scheduler.d.ts +0 -5
- package/dist/runners/scheduler.d.ts.map +0 -1
- package/dist/runners/scheduler.js +0 -19
- package/dist/runners/scheduler.js.map +0 -1
- package/dist/runtime/abort.d.ts +0 -8
- package/dist/runtime/abort.d.ts.map +0 -1
- package/dist/runtime/abort.js +0 -13
- package/dist/runtime/abort.js.map +0 -1
- package/dist/runtime/compatibility.d.ts +0 -37
- package/dist/runtime/compatibility.d.ts.map +0 -1
- package/dist/runtime/compatibility.js +0 -10
- package/dist/runtime/compatibility.js.map +0 -1
- package/dist/runtime/detect.d.ts +0 -13
- package/dist/runtime/detect.d.ts.map +0 -1
- package/dist/runtime/detect.js +0 -18
- package/dist/runtime/detect.js.map +0 -1
- package/dist/runtime/engine.d.ts +0 -20
- package/dist/runtime/engine.d.ts.map +0 -1
- package/dist/runtime/engine.js +0 -61
- package/dist/runtime/engine.js.map +0 -1
- package/dist/runtime/retry.d.ts +0 -19
- package/dist/runtime/retry.d.ts.map +0 -1
- package/dist/runtime/retry.js +0 -54
- package/dist/runtime/retry.js.map +0 -1
- package/dist/runtime/worker.d.ts +0 -10
- package/dist/runtime/worker.d.ts.map +0 -1
- package/dist/runtime/worker.js +0 -25
- package/dist/runtime/worker.js.map +0 -1
- package/dist/scrape/cache.d.ts +0 -45
- package/dist/scrape/cache.d.ts.map +0 -1
- package/dist/scrape/cache.js +0 -45
- package/dist/scrape/cache.js.map +0 -1
- package/dist/scrape/crawl.d.ts +0 -47
- package/dist/scrape/crawl.d.ts.map +0 -1
- package/dist/scrape/crawl.js +0 -114
- package/dist/scrape/crawl.js.map +0 -1
- package/dist/scrape/extract.d.ts +0 -63
- package/dist/scrape/extract.d.ts.map +0 -1
- package/dist/scrape/extract.js +0 -47
- package/dist/scrape/extract.js.map +0 -1
- package/dist/scrape/normalize.d.ts +0 -52
- package/dist/scrape/normalize.d.ts.map +0 -1
- package/dist/scrape/normalize.js +0 -102
- package/dist/scrape/normalize.js.map +0 -1
- package/dist/scrape/robots.d.ts +0 -37
- package/dist/scrape/robots.d.ts.map +0 -1
- package/dist/scrape/robots.js +0 -71
- package/dist/scrape/robots.js.map +0 -1
- package/dist/scrape/schema.d.ts +0 -23
- package/dist/scrape/schema.d.ts.map +0 -1
- package/dist/scrape/schema.js +0 -92
- package/dist/scrape/schema.js.map +0 -1
- package/dist/scrape/scraper.d.ts +0 -6
- package/dist/scrape/scraper.d.ts.map +0 -1
- package/dist/scrape/scraper.js +0 -46
- package/dist/scrape/scraper.js.map +0 -1
- package/dist/scrape/semantic.d.ts +0 -25
- package/dist/scrape/semantic.d.ts.map +0 -1
- package/dist/scrape/semantic.js +0 -27
- package/dist/scrape/semantic.js.map +0 -1
- package/dist/server/node.d.ts +0 -7
- package/dist/server/node.d.ts.map +0 -1
- package/dist/server/node.js.map +0 -1
- package/dist/sessions/file.d.ts +0 -25
- package/dist/sessions/file.d.ts.map +0 -1
- package/dist/sessions/file.js +0 -13
- package/dist/sessions/file.js.map +0 -1
- package/dist/sessions/replay.d.ts +0 -9
- package/dist/sessions/replay.d.ts.map +0 -1
- package/dist/sessions/replay.js +0 -70
- package/dist/sessions/replay.js.map +0 -1
- package/dist/sessions/store.d.ts +0 -25
- package/dist/sessions/store.d.ts.map +0 -1
- package/dist/sessions/store.js +0 -13
- package/dist/sessions/store.js.map +0 -1
- package/dist/storage/adapter.d.ts +0 -5
- package/dist/storage/adapter.d.ts.map +0 -1
- package/dist/storage/adapter.js +0 -11
- package/dist/storage/adapter.js.map +0 -1
- package/dist/storage/cache.d.ts +0 -23
- package/dist/storage/cache.d.ts.map +0 -1
- package/dist/storage/cache.js +0 -79
- package/dist/storage/cache.js.map +0 -1
- package/dist/storage/checksum.d.ts +0 -3
- package/dist/storage/checksum.d.ts.map +0 -1
- package/dist/storage/checksum.js +0 -19
- package/dist/storage/checksum.js.map +0 -1
- package/dist/storage/chunked.d.ts +0 -21
- package/dist/storage/chunked.d.ts.map +0 -1
- package/dist/storage/chunked.js +0 -67
- package/dist/storage/chunked.js.map +0 -1
- package/dist/storage/content.d.ts +0 -24
- package/dist/storage/content.d.ts.map +0 -1
- package/dist/storage/content.js +0 -44
- package/dist/storage/content.js.map +0 -1
- package/dist/storage/filehosting.d.ts +0 -2
- package/dist/storage/filehosting.d.ts.map +0 -1
- package/dist/storage/filehosting.js +0 -24
- package/dist/storage/filehosting.js.map +0 -1
- package/dist/storage/githubcontents.d.ts +0 -2
- package/dist/storage/githubcontents.d.ts.map +0 -1
- package/dist/storage/githubcontents.js +0 -29
- package/dist/storage/githubcontents.js.map +0 -1
- package/dist/storage/index.d.ts +0 -12
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js +0 -12
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/local.d.ts +0 -2
- package/dist/storage/local.d.ts.map +0 -1
- package/dist/storage/local.js +0 -72
- package/dist/storage/local.js.map +0 -1
- package/dist/storage/memory.d.ts +0 -6
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js +0 -36
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/pool.d.ts +0 -95
- package/dist/storage/pool.d.ts.map +0 -1
- package/dist/storage/pool.js +0 -202
- package/dist/storage/pool.js.map +0 -1
- package/dist/storage/s3compatible.d.ts +0 -3
- package/dist/storage/s3compatible.d.ts.map +0 -1
- package/dist/storage/s3compatible.js +0 -70
- package/dist/storage/s3compatible.js.map +0 -1
- package/dist/storage/sync.d.ts +0 -47
- package/dist/storage/sync.d.ts.map +0 -1
- package/dist/storage/sync.js +0 -75
- package/dist/storage/sync.js.map +0 -1
- package/dist/surfaces/adapters.d.ts +0 -97
- package/dist/surfaces/adapters.d.ts.map +0 -1
- package/dist/surfaces/adapters.js +0 -50
- package/dist/surfaces/adapters.js.map +0 -1
- package/dist/surfaces/controls.d.ts +0 -18
- package/dist/surfaces/controls.d.ts.map +0 -1
- package/dist/surfaces/controls.js +0 -45
- package/dist/surfaces/controls.js.map +0 -1
- package/dist/surfaces/manifest.d.ts +0 -68
- package/dist/surfaces/manifest.d.ts.map +0 -1
- package/dist/surfaces/manifest.js +0 -42
- package/dist/surfaces/manifest.js.map +0 -1
- package/dist/surfaces/n8n.d.ts +0 -25
- package/dist/surfaces/n8n.d.ts.map +0 -1
- package/dist/surfaces/n8n.js +0 -31
- package/dist/surfaces/n8n.js.map +0 -1
- package/dist/surfaces/operations.d.ts +0 -32
- package/dist/surfaces/operations.d.ts.map +0 -1
- package/dist/surfaces/operations.js +0 -53
- package/dist/surfaces/operations.js.map +0 -1
- package/dist/surfaces/requirements.d.ts +0 -29
- package/dist/surfaces/requirements.d.ts.map +0 -1
- package/dist/surfaces/requirements.js +0 -50
- package/dist/surfaces/requirements.js.map +0 -1
- package/dist/surfaces/targets.d.ts +0 -196
- package/dist/surfaces/targets.d.ts.map +0 -1
- package/dist/surfaces/targets.js +0 -27
- package/dist/surfaces/targets.js.map +0 -1
- package/dist/webhook/delivery.d.ts +0 -18
- package/dist/webhook/delivery.d.ts.map +0 -1
- package/dist/webhook/delivery.js +0 -36
- package/dist/webhook/delivery.js.map +0 -1
- package/dist/webhook/receiver.d.ts +0 -26
- package/dist/webhook/receiver.d.ts.map +0 -1
- package/dist/webhook/receiver.js +0 -24
- package/dist/webhook/receiver.js.map +0 -1
- package/dist/webhook/signature.d.ts +0 -3
- package/dist/webhook/signature.d.ts.map +0 -1
- package/dist/webhook/signature.js +0 -7
- package/dist/webhook/signature.js.map +0 -1
- package/dist/workflow/manifest.d.ts +0 -22
- package/dist/workflow/manifest.d.ts.map +0 -1
- package/dist/workflow/manifest.js +0 -21
- package/dist/workflow/manifest.js.map +0 -1
- package/dist/workflow/registry.d.ts +0 -7
- package/dist/workflow/registry.d.ts.map +0 -1
- package/dist/workflow/registry.js +0 -18
- package/dist/workflow/registry.js.map +0 -1
- package/dist/workflow/templates.d.ts +0 -6
- package/dist/workflow/templates.d.ts.map +0 -1
- package/dist/workflow/templates.js +0 -18
- package/dist/workflow/templates.js.map +0 -1
- package/dist/workflow/triggers.d.ts +0 -64
- package/dist/workflow/triggers.d.ts.map +0 -1
- package/dist/workflow/triggers.js +0 -99
- package/dist/workflow/triggers.js.map +0 -1
- package/docs/logs/.gitkeep +0 -0
- package/docs/plans/00.index.md +0 -50
- package/docs/plans/01.architecture.md +0 -86
- package/docs/plans/02.research.computer.use.md +0 -58
- package/docs/plans/03.research.captcha.bypass.md +0 -68
- package/docs/plans/04.research.sandbox.ai.md +0 -52
- package/docs/plans/05.capture.platform.md +0 -57
- package/docs/plans/06.dependencies.md +0 -97
- package/docs/plans/07.captcha.test.page.md +0 -41
- package/docs/plans/08.production.infra.md +0 -70
- package/docs/plans/09.database.schema.md +0 -121
- package/docs/plans/10.cloudinary.storage.md +0 -57
- package/docs/plans/11.movement.logs.json.md +0 -72
- package/docs/plans/12.research.atlas.agent.browser.md +0 -79
- package/docs/plans/13.research.anti.detection.md +0 -898
- package/docs/plans/14.research.proxy.md +0 -1495
- package/docs/plans/15.research.retry.rate.limit.md +0 -1958
- package/docs/plans/16.research.crawling.md +0 -1417
- package/docs/plans/17.research.caching.md +0 -1610
- package/docs/plans/18.research.content.extraction.md +0 -1952
- package/docs/plans/19.research.errors.events.md +0 -1523
- package/docs/plans/20.research.zod.validation.md +0 -1350
- package/docs/plans/21.research.batch.concurrency.md +0 -1888
- package/docs/plans/22.research.universal.runtime.md +0 -944
- package/docs/plans/23.research.ai.integration.md +0 -1465
- package/docs/plans/24.research.memory.persistence.md +0 -1979
- package/docs/plans/25.research.server.api.md +0 -342
- package/docs/plans/26.research.compilation.md +0 -249
- package/docs/plans/27.research.html.parsing.md +0 -251
- package/docs/plans/28.action.plan.md +0 -50
- package/docs/plans/29.api.reference.md +0 -174
- package/docs/plans/30.architecture.plan.md +0 -94
- package/docs/plans/31.auditoria.dados.md +0 -163
- package/docs/plans/32.bots.automacao.computacional.md +0 -214
- package/docs/plans/33.bots.codigo.revisao.md +0 -220
- package/docs/plans/34.bots.seguranca.cicd.md +0 -366
- package/docs/plans/35.comparativo.concorrencia.md +0 -464
- package/docs/plans/36.computational.memory.md +0 -340
- package/docs/plans/37.deploystrategy.md +0 -394
- package/docs/plans/38.flow.md +0 -155
- package/docs/plans/39.multi.platform.bot.md +0 -252
- package/docs/plans/40.npm.publish.md +0 -250
- package/docs/plans/41.o.que.falta.md +0 -407
- package/docs/plans/42.pesquisa.concorrencia.md +0 -721
- package/docs/plans/43.plan.universal.architecture.md +0 -496
- package/docs/plans/44.reference.md +0 -100
- package/docs/plans/45.robotarchitecture.md +0 -237
- package/docs/plans/46.scdnintegration.md +0 -284
- package/docs/plans/47.multiforge.readme.md +0 -129
- package/docs/plans/48.theory.v4.repo.os.md +0 -152
- package/docs/plans/49.third.party.infra.md +0 -12
- package/docs/plans/50.file.as.compute.md +0 -39
- package/docs/plans/51.architecture.virtual.processor.md +0 -80
- package/docs/plans/52.manifesto.v8.md +0 -11
- package/docs/plans/58.cdn.list.md +0 -23
- package/docs/plans/59.sql.frameworks.md +0 -33
- package/docs/plans/60.sql.thirdparty.md +0 -26
- package/docs/plans/61.objective.multiforge.md +0 -63
- package/docs/plans/62.huggingface.upload.md +0 -26
- package/docs/plans/63.kaggle.upload.md +0 -24
- package/docs/plans/64.npm.storage.md +0 -30
- package/docs/plans/65.rclone.terabox.md +0 -32
- package/docs/plans/66.buckets.and.models.todo.md +0 -14
- package/docs/plans/67.database.todo.md +0 -13
- package/docs/plans/68.deploy.packages.todo.md +0 -12
- package/docs/plans/69.report.human.operator.md +0 -133
- package/docs/plans/70.report.brain2qwerty.ems.md +0 -135
- package/docs/plans/71.report.hd.infinito.vram.md +0 -155
- package/docs/plans/72.plan.hd.infinito.node.md +0 -146
- package/docs/plans/73.plan.scifi.repos.md +0 -125
- package/docs/plans/74.000.manifesto.v8.flat.2..md +0 -11
- package/docs/plans/README.md +0 -489
- package/docs/plans/aggregate_platforms.mjs +0 -146
- package/docs/plans/examplesession.json +0 -36
- package/docs/plans/missing-facts.md +0 -192
- package/docs/plans/models.md +0 -64
- package/docs/plans/organize.cjs +0 -270
- package/docs/plans/platforms.md +0 -2887
- package/docs/plans/sites.md +0 -31322
- package/docs/sources/farm.py +0 -117
- package/docs/sources/html/saddle1.html +0 -132
- package/docs/sources/html/saddle2.html +0 -157
- package/docs/sources/html/saddle3.html +0 -119
- package/docs/sources/html/saddle4.html +0 -144
- package/docs/sources/html/saddle5.html +0 -72
- package/docs/sources/html/saddle6.html +0 -171
- package/docs/sources/html/saddle7.html +0 -236
- package/docs/sources/saddle.ts +0 -74
- package/docs/sources/schema.prisma +0 -88
- package/docs/sources/script.sh +0 -64
- package/docs/sources/workflows.yml +0 -458
- package/docs/talks1/_body.txt +0 -14
- package/docs/talks1/_index.md +0 -15
- package/docs/talks1/_screenshot.png +0 -0
- package/docs/talks1/assistant-01.md +0 -5
- package/docs/talks1/assistant-02.md +0 -5
- package/docs/talks1/assistant-03.md +0 -531
- package/docs/talks1/assistant-04.md +0 -26
- package/docs/talks1/assistant-05.md +0 -774
- package/docs/talks1/assistant-06.md +0 -1718
- package/docs/talks1/scrape-share.cjs +0 -185
- package/docs/talks1/scrape-share.ts +0 -183
- package/docs/talks1/user-01.md +0 -3
- package/docs/talks1/user-02.md +0 -3
- package/docs/talks1/user-03.md +0 -88
- package/docs/talks1/user-04.md +0 -3
- package/docs/talks1/user-05.md +0 -3
- package/docs/talks1/user-06.md +0 -88
- package/docs/talks1/user-07.md +0 -88
- package/docs/talks2/_body.txt +0 -14
- package/docs/talks2/_index.md +0 -16
- package/docs/talks2/_screenshot.png +0 -0
- package/docs/talks2/assistant-01.md +0 -5
- package/docs/talks2/assistant-02.md +0 -5
- package/docs/talks2/assistant-03.md +0 -424
- package/docs/talks2/assistant-04.md +0 -598
- package/docs/talks2/assistant-05.md +0 -1280
- package/docs/talks2/assistant-06.md +0 -1227
- package/docs/talks2/assistant-07.md +0 -1252
- package/docs/talks2/user-01.md +0 -3
- package/docs/talks2/user-02.md +0 -3
- package/docs/talks2/user-03.md +0 -88
- package/docs/talks2/user-04.md +0 -88
- package/docs/talks2/user-05.md +0 -88
- package/docs/talks2/user-06.md +0 -88
- package/docs/talks2/user-07.md +0 -3
- package/docs/talks3/_body.txt +0 -467
- package/docs/talks3/_index.md +0 -10
- package/docs/talks3/_screenshot.png +0 -0
- package/docs/talks3/assistant-01.md +0 -417
- package/docs/talks3/assistant-02.md +0 -417
- package/docs/talks3/assistant-03.md +0 -29
- package/docs/talks3/assistant-04.md +0 -727
- package/docs/talks3/user-01.md +0 -88
- package/docs/talks3/user-02.md +0 -88
- package/docs/talks3/user-03.md +0 -3
- package/docs/talks3/user-04.md +0 -3
- package/docs/talks4/_body.txt +0 -14
- package/docs/talks4/_index.md +0 -12
- package/docs/talks4/_screenshot.png +0 -0
- package/docs/talks4/assistant-01.md +0 -5
- package/docs/talks4/assistant-02.md +0 -5
- package/docs/talks4/assistant-03.md +0 -35
- package/docs/talks4/assistant-04.md +0 -512
- package/docs/talks4/assistant-05.md +0 -599
- package/docs/talks4/user-01.md +0 -3
- package/docs/talks4/user-02.md +0 -3
- package/docs/talks4/user-03.md +0 -88
- package/docs/talks4/user-04.md +0 -88
- package/docs/talks4/user-05.md +0 -7
- package/docs/talks5/_body.txt +0 -14
- package/docs/talks5/_index.md +0 -13
- package/docs/talks5/_screenshot.png +0 -0
- package/docs/talks5/assistant-01.md +0 -5
- package/docs/talks5/assistant-02.md +0 -5
- package/docs/talks5/assistant-03.md +0 -690
- package/docs/talks5/assistant-04.md +0 -758
- package/docs/talks5/assistant-05.md +0 -974
- package/docs/talks5/user-01.md +0 -3
- package/docs/talks5/user-02.md +0 -3
- package/docs/talks5/user-03.md +0 -105
- package/docs/talks5/user-04.md +0 -105
- package/docs/talks5/user-05.md +0 -63
- package/docs/talks5/user-06.md +0 -105
- package/docs/talks6/_body.txt +0 -14
- package/docs/talks6/_index.md +0 -9
- package/docs/talks6/_screenshot.png +0 -0
- package/docs/talks6/assistant-01.md +0 -5
- package/docs/talks6/assistant-02.md +0 -5
- package/docs/talks6/assistant-03.md +0 -1499
- package/docs/talks6/user-01.md +0 -3
- package/docs/talks6/user-02.md +0 -3
- package/docs/talks6/user-03.md +0 -88
- package/docs/talks6/user-04.md +0 -88
- package/docs/talks7/_body.txt +0 -14
- package/docs/talks7/_index.md +0 -10
- package/docs/talks7/_screenshot.png +0 -0
- package/docs/talks7/assistant-01.md +0 -5
- package/docs/talks7/assistant-02.md +0 -5
- package/docs/talks7/assistant-03.md +0 -523
- package/docs/talks7/assistant-04.md +0 -617
- package/docs/talks7/user-01.md +0 -3
- package/docs/talks7/user-02.md +0 -3
- package/docs/talks7/user-03.md +0 -105
- package/docs/talks7/user-04.md +0 -67
- package/docs/talks8/conversa1.txt +0 -1322
- package/docs/talks8/conversa2.txt +0 -237
- package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +0 -174
- package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +0 -59
- package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +0 -91
- package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +0 -116
- package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +0 -128
- package/docs/talks9/README (2).md +0 -489
- package/docs/talks9/README.md +0 -198
- package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +0 -80
- package/docs/talks9/conversa.txt +0 -544
- package/docs/talks9/other (2).md +0 -39
- package/docs/talks9/other.md +0 -57
- package/docs/talks9/outro.txt +0 -24
- /package/{extension/README.md → docs/extension.md} +0 -0
package/qemubridge.py
ADDED
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qemubridge.py - async QMP bridge client for the virtual hardware engine v2.
|
|
3
|
+
|
|
4
|
+
The module mediates QEMU Machine Protocol (QMP) over a unix socket with a
|
|
5
|
+
single merged class surface: the v6 observer bridge (22 ops) fused with the
|
|
6
|
+
unique operations of the v5 QEMUQMPClient, plus an internal smiadapter module
|
|
7
|
+
that renders a software nvidia-smi facade for a100/h100 flex profiles.
|
|
8
|
+
|
|
9
|
+
Merge provenance (mapcode T12, date-first 2026-08-22):
|
|
10
|
+
- base: e2pool/qemubridge.py (662 lines, v6 observer, 22 functions)
|
|
11
|
+
- absorbed uniques: e2pool/src_qemu_bridge.py (561 lines) - VMStatus enum,
|
|
12
|
+
QEMUConfig loader, MigrationStats polling, oob handshake, hotunplug pair,
|
|
13
|
+
hotpluggable-cpus matching, sched_setaffinity path, query-qemu-features,
|
|
14
|
+
dirty-rate page-sampling mode, qmpsession, batch hotplug, flat configs
|
|
15
|
+
- incorporated as internal module: e2pool/fakenvidiasmi.py (32 lines,
|
|
16
|
+
smiadapter a100/h100 flex table; driver re-pinned 575.51.03 -> 575.57.08)
|
|
17
|
+
- discarded: e2pool/rap.py and e2pool/main.py (v3 boilerplate, no value)
|
|
18
|
+
|
|
19
|
+
Version anchors confirmed 2026-08-23: QEMU 11.1.0, Node 26.7.0 current /
|
|
20
|
+
24.19.0 LTS, npm 12.0.2, NVIDIA driver 575.57.08, CUDA 12.9. Python floor is
|
|
21
|
+
3.14 for production (the file also runs on 3.13); python 3.14 ships the
|
|
22
|
+
incremental garbage collector enabled by default, which only lowers pause
|
|
23
|
+
tail latency.
|
|
24
|
+
|
|
25
|
+
Standalone usage - python3 qemubridge.py --help lists every mode:
|
|
26
|
+
selftest (default, offline) | status | cpus | memory | version | migrate |
|
|
27
|
+
blockjobs | batchcpus | configs | smiadapter | shim
|
|
28
|
+
|
|
29
|
+
The file groups logic in 25 correlated contexts with defensive try/except
|
|
30
|
+
logging throughout. No host is ever hardcoded: the migration target host is
|
|
31
|
+
a parameter or VHE_MIGRATE_HOST, and ports default to random.randint(30000,
|
|
32
|
+
59999). Standard library only (asyncio, json, argparse, pathlib, logging).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# --------------------------------------------------------------------------- #
|
|
36
|
+
# context 01 - module identity and imports
|
|
37
|
+
# --------------------------------------------------------------------------- #
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import argparse
|
|
42
|
+
import asyncio
|
|
43
|
+
import hashlib
|
|
44
|
+
import json
|
|
45
|
+
import logging
|
|
46
|
+
import os
|
|
47
|
+
import random
|
|
48
|
+
import sys
|
|
49
|
+
import time
|
|
50
|
+
from contextlib import asynccontextmanager
|
|
51
|
+
from dataclasses import asdict, dataclass, field
|
|
52
|
+
from enum import Enum
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from typing import Any, Literal
|
|
55
|
+
|
|
56
|
+
# --------------------------------------------------------------------------- #
|
|
57
|
+
# context 02 - version constants (re-pinned, no old pins)
|
|
58
|
+
# --------------------------------------------------------------------------- #
|
|
59
|
+
|
|
60
|
+
PYTHONVERSIONTARGET: str = "3.14" # 3.14+ floor; incremental gc enabled by default
|
|
61
|
+
QEMUVERSIONTARGET: str = "11.1.0"
|
|
62
|
+
NODELTSVERSION: str = "24.19.0"
|
|
63
|
+
NODECURRENTVERSION: str = "26.7.0"
|
|
64
|
+
DRIVERVERSIONTARGET: str = "575.57.08"
|
|
65
|
+
CUDAVERSIONTARGET: str = "12.9"
|
|
66
|
+
BRIDGEVERSION: str = "2.0.0"
|
|
67
|
+
BUILDDATE: str = "2026-08-22"
|
|
68
|
+
|
|
69
|
+
QEMUDEFAULTSOCKET: str = "/run/vhe/vm-{vmid}.qmp"
|
|
70
|
+
QEMUDEFAULTTIMEOUT: float = 8.0
|
|
71
|
+
QEMUDEFAULTBINARY: str = "/usr/bin/qemu-system-x86_64"
|
|
72
|
+
|
|
73
|
+
# --------------------------------------------------------------------------- #
|
|
74
|
+
# context 03 - node compatibility matrix
|
|
75
|
+
# --------------------------------------------------------------------------- #
|
|
76
|
+
|
|
77
|
+
NODECOMPAT: dict[str, Any] = {
|
|
78
|
+
"runtime": f"node@{NODECURRENTVERSION} current / node@{NODELTSVERSION} lts",
|
|
79
|
+
"protocol": "qmp-unix-socket",
|
|
80
|
+
"native_modules": ["node:net", "node:fs", "node:path", "node:crypto", "node:http", "node:child_process"],
|
|
81
|
+
"ipc": "ndjson",
|
|
82
|
+
"socketPath": "/run/vhe/vm.qmp",
|
|
83
|
+
"qemu": QEMUVERSIONTARGET,
|
|
84
|
+
"python": PYTHONVERSIONTARGET,
|
|
85
|
+
"driver": DRIVERVERSIONTARGET,
|
|
86
|
+
"cuda": CUDAVERSIONTARGET,
|
|
87
|
+
"endianness": "little",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# --------------------------------------------------------------------------- #
|
|
91
|
+
# context 04 - logging setup
|
|
92
|
+
# --------------------------------------------------------------------------- #
|
|
93
|
+
|
|
94
|
+
logger = logging.getLogger("qemubridge")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --------------------------------------------------------------------------- #
|
|
98
|
+
# context 05 - status model (typed runstate, 6 states)
|
|
99
|
+
# --------------------------------------------------------------------------- #
|
|
100
|
+
|
|
101
|
+
class VMStatus(str, Enum):
|
|
102
|
+
"""Maps the QEMU query-status runstate vocabulary."""
|
|
103
|
+
|
|
104
|
+
RUNNING = "running"
|
|
105
|
+
PAUSED = "paused"
|
|
106
|
+
SHUTDOWN = "shutdown"
|
|
107
|
+
INMIGRATE = "inmigrate"
|
|
108
|
+
PRELAUNCH = "prelaunch"
|
|
109
|
+
SUSPENDED = "suspended"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# --------------------------------------------------------------------------- #
|
|
113
|
+
# context 06 - error model (code plus details)
|
|
114
|
+
# --------------------------------------------------------------------------- #
|
|
115
|
+
|
|
116
|
+
class QemuBridgeError(Exception):
|
|
117
|
+
"""Carries a QMP error class code and a details payload for operators."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, message: str, code: str = "QMP_ERROR", details: dict[str, Any] | None = None):
|
|
120
|
+
super().__init__(message)
|
|
121
|
+
self.code = code
|
|
122
|
+
self.details = details or {}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --------------------------------------------------------------------------- #
|
|
126
|
+
# context 07 - stats model (bridge counters)
|
|
127
|
+
# --------------------------------------------------------------------------- #
|
|
128
|
+
|
|
129
|
+
@dataclass(slots=True)
|
|
130
|
+
class QemuBridgeStats:
|
|
131
|
+
"""Tracks bridge counters across the connection lifetime."""
|
|
132
|
+
|
|
133
|
+
connects: int = 0
|
|
134
|
+
commands: int = 0
|
|
135
|
+
errors: int = 0
|
|
136
|
+
lastlatencyms: float = 0.0
|
|
137
|
+
startedat: float = field(default_factory=time.time)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --------------------------------------------------------------------------- #
|
|
141
|
+
# context 08 - config model (qemu.config / vm.config loader)
|
|
142
|
+
# --------------------------------------------------------------------------- #
|
|
143
|
+
|
|
144
|
+
def coercebool(value: Any) -> bool:
|
|
145
|
+
"""Coerces config scalars to bool without the truthy-string trap."""
|
|
146
|
+
if isinstance(value, str):
|
|
147
|
+
return value.strip().lower() in ("1", "true", "yes", "on")
|
|
148
|
+
return bool(value)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(slots=True)
|
|
152
|
+
class QEMUConfig:
|
|
153
|
+
"""Holds the parsed representation of qemu.config or vm.config."""
|
|
154
|
+
|
|
155
|
+
vmid: str
|
|
156
|
+
socketpath: str = ""
|
|
157
|
+
qemubinary: str = QEMUDEFAULTBINARY
|
|
158
|
+
machinetype: str = "q35"
|
|
159
|
+
accel: str = "kvm"
|
|
160
|
+
cpumodel: str = "host"
|
|
161
|
+
cores: int = 8
|
|
162
|
+
vcpus: int = 8
|
|
163
|
+
memorymb: int = 8192
|
|
164
|
+
hugepages: bool = False
|
|
165
|
+
cxlenabled: bool = False
|
|
166
|
+
gpupassthrough: list[str] = field(default_factory=list)
|
|
167
|
+
extraargs: list[str] = field(default_factory=list)
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def loadfromfile(cls, path: str | os.PathLike, vmid: str = "vm0") -> QEMUConfig:
|
|
171
|
+
"""
|
|
172
|
+
Loads a json or key=value config file with defensive defaults.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
path: Config file location; a missing file yields safe defaults.
|
|
176
|
+
vmid: VM identifier used for the default socket path.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
QEMUConfig: The parsed configuration with per-key fallbacks.
|
|
180
|
+
"""
|
|
181
|
+
p = Path(path)
|
|
182
|
+
data: dict[str, Any] = {"vm_id": vmid}
|
|
183
|
+
if p.exists():
|
|
184
|
+
try:
|
|
185
|
+
if p.suffix == ".json":
|
|
186
|
+
loaded = json.loads(p.read_text())
|
|
187
|
+
if isinstance(loaded, dict):
|
|
188
|
+
data.update(loaded)
|
|
189
|
+
else:
|
|
190
|
+
for line in p.read_text().splitlines():
|
|
191
|
+
line = line.strip()
|
|
192
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
193
|
+
continue
|
|
194
|
+
k, _, v = line.partition("=")
|
|
195
|
+
data[k.strip()] = v.strip().strip('"').strip("'")
|
|
196
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
197
|
+
logger.warning("failed to parse %s: %s - using defaults", path, e)
|
|
198
|
+
return cls(
|
|
199
|
+
vmid=str(data.get("vm_id", vmid)),
|
|
200
|
+
socketpath=str(data.get("socket_path", QEMUDEFAULTSOCKET.format(vmid=vmid))),
|
|
201
|
+
qemubinary=str(data.get("qemu_binary", QEMUDEFAULTBINARY)),
|
|
202
|
+
machinetype=str(data.get("machine_type", "q35")),
|
|
203
|
+
accel=str(data.get("accel", "kvm")),
|
|
204
|
+
cpumodel=str(data.get("cpu_model", "host")),
|
|
205
|
+
cores=int(data.get("cores", data.get("vcpus", 8))),
|
|
206
|
+
vcpus=int(data.get("vcpus", 8)),
|
|
207
|
+
memorymb=int(data.get("memory_mb", 8192)),
|
|
208
|
+
hugepages=coercebool(data.get("hugepages", False)),
|
|
209
|
+
cxlenabled=coercebool(data.get("cxl_enabled", False)),
|
|
210
|
+
gpupassthrough=list(data.get("gpu_passthrough", [])),
|
|
211
|
+
extraargs=list(data.get("extra_args", [])),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# --------------------------------------------------------------------------- #
|
|
216
|
+
# context 09 - migration model (stats payload)
|
|
217
|
+
# --------------------------------------------------------------------------- #
|
|
218
|
+
|
|
219
|
+
@dataclass(slots=True)
|
|
220
|
+
class MigrationStats:
|
|
221
|
+
"""Summarizes one live migration attempt for callers and audits."""
|
|
222
|
+
|
|
223
|
+
ramtransferredmb: float
|
|
224
|
+
ramremainingmb: float
|
|
225
|
+
dirtyratembps: float
|
|
226
|
+
downtimems: int
|
|
227
|
+
status: str
|
|
228
|
+
uri: str = ""
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# --------------------------------------------------------------------------- #
|
|
232
|
+
# context 10 - bridge transport internals (envelope, events, oob)
|
|
233
|
+
# --------------------------------------------------------------------------- #
|
|
234
|
+
|
|
235
|
+
class QemuBridge:
|
|
236
|
+
"""
|
|
237
|
+
Implements the async QMP client for QEMU 11.1.0 with 25 numbered ops.
|
|
238
|
+
|
|
239
|
+
The bridge negotiates capabilities (oob enabled), queries VM state and
|
|
240
|
+
performs hotplug, hotunplug, snapshots, migration, block jobs, vCPU
|
|
241
|
+
affinity and IO throttling. CI tolerance: a missing socket produces a
|
|
242
|
+
logged mock connect instead of a crash.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
def __init__(self, socketpath: str = "/run/vhe/vm.qmp", timeout: float = QEMUDEFAULTTIMEOUT):
|
|
246
|
+
self.socketpath: str = socketpath
|
|
247
|
+
self.timeout: float = timeout
|
|
248
|
+
self.reader: asyncio.StreamReader | None = None
|
|
249
|
+
self.writer: asyncio.StreamWriter | None = None
|
|
250
|
+
self.stats = QemuBridgeStats()
|
|
251
|
+
self.handshaked: bool = False
|
|
252
|
+
self.qemurelease: str = QEMUVERSIONTARGET # attr qemurelease: method qemuversion() keeps the op name
|
|
253
|
+
self.sendlock = asyncio.Lock()
|
|
254
|
+
|
|
255
|
+
async def send(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Writes one NDJSON command and returns the full response envelope.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
payload: QMP command dictionary with an ``execute`` key.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
dict: The full QMP response envelope, events already skipped.
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
QemuBridgeError: On transport failure, timeout or QMP error reply.
|
|
267
|
+
"""
|
|
268
|
+
if not self.writer or not self.reader:
|
|
269
|
+
raise QemuBridgeError("not connected", "NOT_CONNECTED")
|
|
270
|
+
async with self.sendlock:
|
|
271
|
+
start = time.perf_counter()
|
|
272
|
+
try:
|
|
273
|
+
self.writer.write((json.dumps(payload) + "\n").encode("utf-8"))
|
|
274
|
+
await self.writer.drain()
|
|
275
|
+
while True:
|
|
276
|
+
raw = await asyncio.wait_for(self.reader.readline(), timeout=self.timeout)
|
|
277
|
+
if not raw:
|
|
278
|
+
raise QemuBridgeError("empty response", "EMPTY_RESPONSE")
|
|
279
|
+
try:
|
|
280
|
+
resp = json.loads(raw.decode("utf-8"))
|
|
281
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
282
|
+
logger.debug("skipping non-json QMP line")
|
|
283
|
+
continue
|
|
284
|
+
match resp:
|
|
285
|
+
case {"event": str() as event}:
|
|
286
|
+
logger.debug("qmp event skipped: %s", event)
|
|
287
|
+
continue # async events interleave with responses
|
|
288
|
+
case {"error": err}:
|
|
289
|
+
raise QemuBridgeError(
|
|
290
|
+
err.get("desc", "qmp error"), err.get("class", "QMP_ERROR"), err
|
|
291
|
+
)
|
|
292
|
+
case {"return": _}:
|
|
293
|
+
self.stats.commands += 1
|
|
294
|
+
self.stats.lastlatencyms = (time.perf_counter() - start) * 1000.0
|
|
295
|
+
return resp
|
|
296
|
+
case _:
|
|
297
|
+
self.stats.commands += 1 # bare ack tolerance
|
|
298
|
+
return resp
|
|
299
|
+
except asyncio.TimeoutError as e:
|
|
300
|
+
self.stats.errors += 1
|
|
301
|
+
raise QemuBridgeError(f"timeout after {self.timeout}s", "TIMEOUT") from e
|
|
302
|
+
except QemuBridgeError:
|
|
303
|
+
self.stats.errors += 1
|
|
304
|
+
raise
|
|
305
|
+
except Exception as e:
|
|
306
|
+
self.stats.errors += 1
|
|
307
|
+
raise QemuBridgeError(str(e), "TRANSPORT_ERROR", {"exception": repr(e)}) from e
|
|
308
|
+
|
|
309
|
+
async def readgreeting(self) -> dict[str, Any]:
|
|
310
|
+
"""Reads the QMP greeting banner sent right after socket connect."""
|
|
311
|
+
if not self.reader:
|
|
312
|
+
raise QemuBridgeError("no reader", "NOT_CONNECTED")
|
|
313
|
+
raw = await asyncio.wait_for(self.reader.readline(), timeout=self.timeout)
|
|
314
|
+
if not raw:
|
|
315
|
+
raise QemuBridgeError("no greeting", "GREETING_FAILED")
|
|
316
|
+
return json.loads(raw.decode("utf-8"))
|
|
317
|
+
|
|
318
|
+
async def execute(self, command: str, arguments: dict[str, Any] | None = None) -> Any:
|
|
319
|
+
"""
|
|
320
|
+
Executes one QMP command and unwraps the return payload.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
command: QMP command name, for example ``query-status``.
|
|
324
|
+
arguments: Optional command arguments dictionary.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Any: The ``return`` field of the QMP response when present.
|
|
328
|
+
"""
|
|
329
|
+
payload: dict[str, Any] = {"execute": command}
|
|
330
|
+
if arguments:
|
|
331
|
+
payload["arguments"] = arguments
|
|
332
|
+
resp = await self.send(payload)
|
|
333
|
+
return resp.get("return", resp)
|
|
334
|
+
|
|
335
|
+
def getnodecompat(self) -> dict[str, Any]:
|
|
336
|
+
"""Returns the node compatibility descriptor of the bridge."""
|
|
337
|
+
return {**NODECOMPAT, "bridge": BRIDGEVERSION, "date": BUILDDATE}
|
|
338
|
+
|
|
339
|
+
# ----------------------------------------------------------------------- #
|
|
340
|
+
# context 11 - connectivity lifecycle
|
|
341
|
+
# ----------------------------------------------------------------------- #
|
|
342
|
+
|
|
343
|
+
async def connect(self) -> bool:
|
|
344
|
+
"""Op 01/25 - establishes the unix socket link and reads the greeting."""
|
|
345
|
+
try:
|
|
346
|
+
if not os.path.exists(self.socketpath):
|
|
347
|
+
# The bridge tolerates a missing socket in CI with a mock link.
|
|
348
|
+
logger.warning("socket %s absent - mock connect for CI %s", self.socketpath, BUILDDATE)
|
|
349
|
+
self.stats.connects += 1
|
|
350
|
+
return True
|
|
351
|
+
self.reader, self.writer = await asyncio.wait_for(
|
|
352
|
+
asyncio.open_unix_connection(self.socketpath), timeout=self.timeout
|
|
353
|
+
)
|
|
354
|
+
self.stats.connects += 1
|
|
355
|
+
greeting = await self.readgreeting()
|
|
356
|
+
qver = greeting.get("QMP", {}).get("version", {}).get("qemu", {})
|
|
357
|
+
self.qemurelease = f"{qver.get('major', 11)}.{qver.get('minor', 1)}.{qver.get('micro', 0)}"
|
|
358
|
+
logger.info("connected QMP greeting %s", greeting.get("QMP", {}).get("version", {}))
|
|
359
|
+
return True
|
|
360
|
+
except Exception as e:
|
|
361
|
+
raise QemuBridgeError(f"connect failed {e}", "CONNECT_FAILED", {"path": self.socketpath}) from e
|
|
362
|
+
|
|
363
|
+
async def handshake(self) -> dict[str, Any]:
|
|
364
|
+
"""Op 02/25 - negotiates capabilities with oob and validates the version."""
|
|
365
|
+
resp = await self.send({"execute": "qmp_capabilities", "arguments": {"enable": ["oob"]}})
|
|
366
|
+
versionresp = await self.send({"execute": "query-version"})
|
|
367
|
+
qemu = versionresp.get("return", {}).get("qemu", {})
|
|
368
|
+
self.qemurelease = f"{qemu.get('major', 11)}.{qemu.get('minor', 1)}.{qemu.get('micro', 0)}"
|
|
369
|
+
self.handshaked = True
|
|
370
|
+
return {"capabilities": resp.get("return", {}), "version": versionresp.get("return", {}), "qemu_target": QEMUVERSIONTARGET}
|
|
371
|
+
|
|
372
|
+
async def disconnect(self) -> bool:
|
|
373
|
+
"""Op 25/25 - sends quit and gracefully closes the QMP session."""
|
|
374
|
+
try:
|
|
375
|
+
if self.writer and self.handshaked:
|
|
376
|
+
try:
|
|
377
|
+
await self.send({"execute": "quit"})
|
|
378
|
+
except Exception:
|
|
379
|
+
logger.debug("quit not acknowledged before close")
|
|
380
|
+
if self.writer:
|
|
381
|
+
self.writer.close()
|
|
382
|
+
try:
|
|
383
|
+
await self.writer.wait_closed()
|
|
384
|
+
except Exception:
|
|
385
|
+
logger.debug("wait_closed failed - socket already gone")
|
|
386
|
+
self.reader = None
|
|
387
|
+
self.writer = None
|
|
388
|
+
self.handshaked = False
|
|
389
|
+
return True
|
|
390
|
+
except Exception as e:
|
|
391
|
+
raise QemuBridgeError(f"disconnect failed {e}", "DISCONNECT_FAILED") from e
|
|
392
|
+
|
|
393
|
+
async def __aenter__(self) -> QemuBridge:
|
|
394
|
+
await self.connect()
|
|
395
|
+
try:
|
|
396
|
+
await self.handshake()
|
|
397
|
+
except Exception:
|
|
398
|
+
logger.warning("handshake deferred - CI mock mode")
|
|
399
|
+
return self
|
|
400
|
+
|
|
401
|
+
async def __aexit__(self, exctype, exc, tb) -> None: # type: ignore[no-untyped-def]
|
|
402
|
+
await self.disconnect()
|
|
403
|
+
|
|
404
|
+
# ----------------------------------------------------------------------- #
|
|
405
|
+
# context 12 - vm introspection
|
|
406
|
+
# ----------------------------------------------------------------------- #
|
|
407
|
+
|
|
408
|
+
async def querystatus(self) -> VMStatus:
|
|
409
|
+
"""Op 04/25 - returns the typed VM runstate with a running fallback."""
|
|
410
|
+
detail = await self.execute("query-status")
|
|
411
|
+
status = detail.get("status", "running") if isinstance(detail, dict) else "running"
|
|
412
|
+
try:
|
|
413
|
+
return VMStatus(status)
|
|
414
|
+
except ValueError:
|
|
415
|
+
logger.warning("unknown runstate %r - defaulting to running", status)
|
|
416
|
+
return VMStatus.RUNNING
|
|
417
|
+
|
|
418
|
+
async def querycpus(self) -> list[dict[str, Any]]:
|
|
419
|
+
"""Op 05/25 - lists vCPUs via query-cpus-fast with legacy fallback."""
|
|
420
|
+
try:
|
|
421
|
+
return await self.execute("query-cpus-fast")
|
|
422
|
+
except QemuBridgeError as e:
|
|
423
|
+
logger.warning("query-cpus-fast failed (%s) - falling back to query-cpus", e.code)
|
|
424
|
+
return await self.execute("query-cpus")
|
|
425
|
+
|
|
426
|
+
async def querymemory(self) -> dict[str, Any]:
|
|
427
|
+
"""Op 06/25 - summarizes memory size, devices and balloon state."""
|
|
428
|
+
summary = await self.execute("query-memory-size-summary")
|
|
429
|
+
try:
|
|
430
|
+
devices = await self.execute("query-memory-devices")
|
|
431
|
+
except QemuBridgeError as e:
|
|
432
|
+
logger.debug("query-memory-devices unavailable: %s", e.code)
|
|
433
|
+
devices = []
|
|
434
|
+
try:
|
|
435
|
+
balloon = await self.execute("query-balloon")
|
|
436
|
+
except QemuBridgeError as e:
|
|
437
|
+
logger.debug("query-balloon unavailable: %s", e.code)
|
|
438
|
+
balloon = {"actual": summary.get("base-memory", 0)} if isinstance(summary, dict) else {}
|
|
439
|
+
return {
|
|
440
|
+
"summary": summary,
|
|
441
|
+
"devices": devices,
|
|
442
|
+
"balloon": balloon,
|
|
443
|
+
"timestamp": time.time(), # typo " timestamp" from the v5 source fixed here
|
|
444
|
+
"target": QEMUVERSIONTARGET,
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# ----------------------------------------------------------------------- #
|
|
448
|
+
# context 13 - cpu hotplug and hotunplug
|
|
449
|
+
# ----------------------------------------------------------------------- #
|
|
450
|
+
|
|
451
|
+
async def hotplugcpu(self, socketid: int = 0, coreid: int = 1, threadid: int = 0) -> dict[str, Any]:
|
|
452
|
+
"""
|
|
453
|
+
Op 07/25 - hotplugs a vCPU matching the hotpluggable-cpus topology.
|
|
454
|
+
|
|
455
|
+
The bridge prefers the cpu type advertised by the matching slot and
|
|
456
|
+
falls back to the host cpu model, which beats fabricated id suffixes.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
socketid: Target socket index advertised by the topology.
|
|
460
|
+
coreid: Target core index inside the socket.
|
|
461
|
+
threadid: Target thread index inside the core.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
dict: The device id, driver, resolved props and the action tag.
|
|
465
|
+
"""
|
|
466
|
+
target: dict[str, Any] | None = None
|
|
467
|
+
try:
|
|
468
|
+
hotpluggable = await self.execute("query-hotpluggable-cpus")
|
|
469
|
+
except QemuBridgeError as e:
|
|
470
|
+
logger.warning("query-hotpluggable-cpus failed (%s) - using props defaults", e.code)
|
|
471
|
+
hotpluggable = []
|
|
472
|
+
for cpu in hotpluggable:
|
|
473
|
+
props = cpu.get("props", {})
|
|
474
|
+
if props.get("socket-id") == socketid and props.get("core-id") == coreid:
|
|
475
|
+
target = cpu
|
|
476
|
+
break
|
|
477
|
+
if target is None and hotpluggable:
|
|
478
|
+
target = hotpluggable[0]
|
|
479
|
+
props = (target or {}).get("props", {}) or {"socket-id": socketid, "core-id": coreid, "thread-id": threadid}
|
|
480
|
+
driver = (target or {}).get("type", "host-x86_64-cpu")
|
|
481
|
+
cpuid = f"cpu-{props.get('socket-id', socketid)}-{props.get('core-id', coreid)}-{props.get('thread-id', threadid)}"
|
|
482
|
+
await self.execute("device_add", {
|
|
483
|
+
"driver": driver,
|
|
484
|
+
"id": cpuid,
|
|
485
|
+
"socket-id": props.get("socket-id", socketid),
|
|
486
|
+
"core-id": props.get("core-id", coreid),
|
|
487
|
+
"thread-id": props.get("thread-id", threadid),
|
|
488
|
+
})
|
|
489
|
+
return {"id": cpuid, "driver": driver, "props": props, "action": "hotplug_cpu"}
|
|
490
|
+
|
|
491
|
+
async def hotunplugcpu(self, cpuid: str) -> Any:
|
|
492
|
+
"""Op 08/25 - removes a vCPU via device_del (guest cooperation)."""
|
|
493
|
+
return await self.execute("device_del", {"id": cpuid})
|
|
494
|
+
|
|
495
|
+
# ----------------------------------------------------------------------- #
|
|
496
|
+
# context 14 - memory hotplug
|
|
497
|
+
# ----------------------------------------------------------------------- #
|
|
498
|
+
|
|
499
|
+
async def hotplugmemory(self, sizemb: int = 1024, node: int = 0, memid: str | None = None) -> dict[str, Any]:
|
|
500
|
+
"""
|
|
501
|
+
Op 09/25 - hotplugs a pc-dimm backed by memory-backend-ram (>=128MB).
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
sizemb: Dimm size in megabytes; values below 128 raise EINVAL.
|
|
505
|
+
node: NUMA node the dimm attaches to.
|
|
506
|
+
memid: Optional backend id; defaults to a timestamped name.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
dict: The backend id, dimm id, size and node of the new device.
|
|
510
|
+
|
|
511
|
+
Raises:
|
|
512
|
+
QemuBridgeError: When the requested size is below the 128MB floor.
|
|
513
|
+
"""
|
|
514
|
+
if sizemb < 128:
|
|
515
|
+
raise QemuBridgeError("size too small (min 128MB)", "EINVAL", {"size_mb": sizemb})
|
|
516
|
+
stamp = int(time.time()) % 100000
|
|
517
|
+
backendid = memid or f"mem{stamp}"
|
|
518
|
+
dimmid = f"dimm{stamp}"
|
|
519
|
+
await self.execute("object-add", {
|
|
520
|
+
"qom-type": "memory-backend-ram",
|
|
521
|
+
"id": backendid,
|
|
522
|
+
"size": sizemb * 1024 * 1024,
|
|
523
|
+
})
|
|
524
|
+
await self.execute("device_add", {
|
|
525
|
+
"driver": "pc-dimm",
|
|
526
|
+
"id": dimmid,
|
|
527
|
+
"memdev": backendid,
|
|
528
|
+
"node": node,
|
|
529
|
+
})
|
|
530
|
+
return {"memdev": backendid, "dimm": dimmid, "size_mb": sizemb, "node": node}
|
|
531
|
+
|
|
532
|
+
# ----------------------------------------------------------------------- #
|
|
533
|
+
# context 15 - pcie hotplug and hotunplug
|
|
534
|
+
# ----------------------------------------------------------------------- #
|
|
535
|
+
|
|
536
|
+
async def hotplugpcie(
|
|
537
|
+
self,
|
|
538
|
+
driver: str = "nvme",
|
|
539
|
+
bus: str = "pcie.0",
|
|
540
|
+
idstr: str | None = None,
|
|
541
|
+
addr: str = "0x0",
|
|
542
|
+
) -> dict[str, Any]:
|
|
543
|
+
"""Op 10/25 - hotplugs a PCIe device with a sha1-derived stable id."""
|
|
544
|
+
devid = idstr or f"pcie-{driver}-{hashlib.sha1(os.urandom(8)).hexdigest()[:6]}"
|
|
545
|
+
args: dict[str, Any] = {"driver": driver, "id": devid, "bus": bus, "addr": addr}
|
|
546
|
+
if driver == "vfio-pci":
|
|
547
|
+
args["x-vga"] = "gpu" in devid
|
|
548
|
+
await self.execute("device_add", args)
|
|
549
|
+
return {"id": devid, "driver": driver, "bus": bus, "addr": addr}
|
|
550
|
+
|
|
551
|
+
async def hotunplugpcie(self, deviceid: str) -> Any:
|
|
552
|
+
"""Op 11/25 - removes a PCIe device via device_del."""
|
|
553
|
+
return await self.execute("device_del", {"id": deviceid})
|
|
554
|
+
|
|
555
|
+
# ----------------------------------------------------------------------- #
|
|
556
|
+
# context 16 - internal snapshots
|
|
557
|
+
# ----------------------------------------------------------------------- #
|
|
558
|
+
|
|
559
|
+
async def snapshotcreate(self, name: str, devices: list[str] | None = None) -> dict[str, Any]:
|
|
560
|
+
"""Op 12/25 - creates a snapshot via snapshot-save with savevm fallback."""
|
|
561
|
+
if not name or len(name) > 128:
|
|
562
|
+
raise QemuBridgeError("invalid snapshot name", "EINVAL", {"name": name})
|
|
563
|
+
try:
|
|
564
|
+
result = await self.execute("snapshot-save", {
|
|
565
|
+
"job-id": f"snap-{name}",
|
|
566
|
+
"tag": name,
|
|
567
|
+
"vmstate": "vmstate",
|
|
568
|
+
"devices": devices or ["drv0"],
|
|
569
|
+
})
|
|
570
|
+
except QemuBridgeError as e:
|
|
571
|
+
logger.warning("snapshot-save unavailable (%s) - savevm fallback", e.code)
|
|
572
|
+
result = await self.execute("human-monitor-command", {"command-line": f"savevm {name}"})
|
|
573
|
+
return {"name": name, "result": result, "date": BUILDDATE}
|
|
574
|
+
|
|
575
|
+
async def snapshotdelete(self, name: str) -> dict[str, Any]:
|
|
576
|
+
"""Op 13/25 - deletes a snapshot via snapshot-delete with delvm fallback."""
|
|
577
|
+
try:
|
|
578
|
+
result = await self.execute("snapshot-delete", {"job-id": f"del-{name}", "tag": name})
|
|
579
|
+
except QemuBridgeError as e:
|
|
580
|
+
logger.warning("snapshot-delete unavailable (%s) - delvm fallback", e.code)
|
|
581
|
+
result = await self.execute("human-monitor-command", {"command-line": f"delvm {name}"})
|
|
582
|
+
return {"name": name, "deleted": True, "result": result}
|
|
583
|
+
|
|
584
|
+
async def snapshotrestore(self, name: str) -> dict[str, Any]:
|
|
585
|
+
"""Op 14/25 - loads a snapshot via snapshot-load with loadvm fallback."""
|
|
586
|
+
try:
|
|
587
|
+
result = await self.execute("snapshot-load", {"job-id": f"load-{name}", "tag": name})
|
|
588
|
+
except QemuBridgeError as e:
|
|
589
|
+
logger.warning("snapshot-load unavailable (%s) - loadvm fallback", e.code)
|
|
590
|
+
result = await self.execute("human-monitor-command", {"command-line": f"loadvm {name}"})
|
|
591
|
+
return {"name": name, "restored": True, "result": result}
|
|
592
|
+
|
|
593
|
+
# ----------------------------------------------------------------------- #
|
|
594
|
+
# context 17 - migration control (multifd, polling, status)
|
|
595
|
+
# ----------------------------------------------------------------------- #
|
|
596
|
+
|
|
597
|
+
def randomport(self) -> int:
|
|
598
|
+
"""Returns a random migration port in the 30000-59999 user range."""
|
|
599
|
+
return random.randint(30000, 59999)
|
|
600
|
+
|
|
601
|
+
async def livemigrate(
|
|
602
|
+
self,
|
|
603
|
+
uri: str | None = None,
|
|
604
|
+
host: str | None = None,
|
|
605
|
+
port: int | None = None,
|
|
606
|
+
multifdchannels: int = 8,
|
|
607
|
+
downtimems: int = 300,
|
|
608
|
+
bandwidth: int = 1073741824,
|
|
609
|
+
wait: bool = True,
|
|
610
|
+
) -> MigrationStats:
|
|
611
|
+
"""
|
|
612
|
+
Op 15/25 - runs a live migration and polls until completed.
|
|
613
|
+
|
|
614
|
+
The destination is explicit: either a full uri or a host parameter
|
|
615
|
+
(or VHE_MIGRATE_HOST); the port defaults to a random 30000-59999
|
|
616
|
+
draw. The bridge never falls back to an implicit localhost target.
|
|
617
|
+
|
|
618
|
+
Args:
|
|
619
|
+
uri: Full migration uri, for example ``tcp:host:port``.
|
|
620
|
+
host: Target host; combined with a random or explicit port.
|
|
621
|
+
port: Optional explicit port; otherwise a random draw.
|
|
622
|
+
multifdchannels: Multifd channel count, clamped to the 2-32 range.
|
|
623
|
+
downtimems: Maximum acceptable guest downtime in milliseconds.
|
|
624
|
+
bandwidth: Migration bandwidth cap in bytes per second.
|
|
625
|
+
wait: When true, polls query-migrate until completion.
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
MigrationStats: Transfer counters, downtime and final status.
|
|
629
|
+
|
|
630
|
+
Raises:
|
|
631
|
+
QemuBridgeError: On invalid ranges, failure or poll timeout.
|
|
632
|
+
"""
|
|
633
|
+
if not (2 <= multifdchannels <= 32):
|
|
634
|
+
raise QemuBridgeError("multifdchannels out of range 2-32", "EINVAL", {"channels": multifdchannels})
|
|
635
|
+
if uri is None:
|
|
636
|
+
targethost = host or os.environ.get("VHE_MIGRATE_HOST")
|
|
637
|
+
if not targethost:
|
|
638
|
+
raise QemuBridgeError("migration target required: pass uri or host (no implicit localhost)", "EINVAL")
|
|
639
|
+
uri = f"tcp:{targethost}:{port if port is not None else self.randomport()}"
|
|
640
|
+
await self.execute("migrate-set-parameters", {
|
|
641
|
+
"max-bandwidth": bandwidth,
|
|
642
|
+
"downtime-limit": downtimems,
|
|
643
|
+
"multifd-channels": multifdchannels,
|
|
644
|
+
})
|
|
645
|
+
await self.execute("migrate", {"uri": uri})
|
|
646
|
+
if not wait:
|
|
647
|
+
return MigrationStats(0.0, 0.0, 0.0, 0, "launched", uri)
|
|
648
|
+
for _ in range(120): # poll up to 120 x 1s for completion
|
|
649
|
+
await asyncio.sleep(1.0)
|
|
650
|
+
status = await self.execute("query-migrate")
|
|
651
|
+
state = status.get("status", "active") if isinstance(status, dict) else "active"
|
|
652
|
+
if state == "completed":
|
|
653
|
+
ram = status.get("ram", {})
|
|
654
|
+
pagesize = 4096 # 4 KiB guest page assumption for the rate math
|
|
655
|
+
return MigrationStats(
|
|
656
|
+
ramtransferredmb=ram.get("transferred", 0) / (1024 * 1024),
|
|
657
|
+
ramremainingmb=ram.get("remaining", 0) / (1024 * 1024),
|
|
658
|
+
dirtyratembps=ram.get("dirty-pages-rate", 0) * pagesize / (1024 * 1024),
|
|
659
|
+
downtimems=int(status.get("downtime", downtimems)),
|
|
660
|
+
status="completed",
|
|
661
|
+
uri=uri,
|
|
662
|
+
)
|
|
663
|
+
if state == "failed":
|
|
664
|
+
raise QemuBridgeError("migration failed", "MIGRATION_FAILED", status)
|
|
665
|
+
raise QemuBridgeError("migration timed out after 120s poll", "MIGRATION_TIMEOUT", {"uri": uri})
|
|
666
|
+
|
|
667
|
+
async def setmultifd(self, channels: int = 8, compression: str = "zstd", zerocopy: bool = True) -> dict[str, Any]:
|
|
668
|
+
"""Op 16/25 - configures multifd capabilities, channels and zstd level."""
|
|
669
|
+
if not (2 <= channels <= 32):
|
|
670
|
+
raise QemuBridgeError("multifd channels out of range 2-32", "EINVAL", {"channels": channels})
|
|
671
|
+
if compression not in ("none", "zlib", "zstd"):
|
|
672
|
+
raise QemuBridgeError("unknown multifd compression", "EINVAL", {"compression": compression})
|
|
673
|
+
await self.execute("migrate-set-capabilities", {
|
|
674
|
+
"capabilities": [
|
|
675
|
+
{"capability": "multifd", "state": True},
|
|
676
|
+
{"capability": "zero-copy-send", "state": zerocopy},
|
|
677
|
+
]
|
|
678
|
+
})
|
|
679
|
+
result = await self.execute("migrate-set-parameters", {
|
|
680
|
+
"multifd-channels": channels,
|
|
681
|
+
"multifd-compression": compression,
|
|
682
|
+
"multifd-zstd-level": 3,
|
|
683
|
+
})
|
|
684
|
+
return {"channels": channels, "compression": compression, "zero_copy": zerocopy, "result": result}
|
|
685
|
+
|
|
686
|
+
async def querymigratestatus(self) -> dict[str, Any]:
|
|
687
|
+
"""Op 24/25 - queries migration status with capability merge."""
|
|
688
|
+
status = await self.execute("query-migrate")
|
|
689
|
+
if not isinstance(status, dict):
|
|
690
|
+
return {"status": "unknown"}
|
|
691
|
+
try:
|
|
692
|
+
caps = await self.execute("query-migrate-capabilities")
|
|
693
|
+
status["capabilities"] = caps
|
|
694
|
+
except QemuBridgeError as e:
|
|
695
|
+
logger.debug("query-migrate-capabilities unavailable: %s", e.code)
|
|
696
|
+
return status
|
|
697
|
+
|
|
698
|
+
# ----------------------------------------------------------------------- #
|
|
699
|
+
# context 18 - dirty page rate sampling
|
|
700
|
+
# ----------------------------------------------------------------------- #
|
|
701
|
+
|
|
702
|
+
async def calcdirtyrate(self, periodsec: int = 1, mode: str = "page-sampling") -> dict[str, Any]:
|
|
703
|
+
"""Op 17/25 - samples the guest dirty page rate (1-60s window)."""
|
|
704
|
+
if not (1 <= periodsec <= 60):
|
|
705
|
+
raise QemuBridgeError("period out of range 1-60", "EINVAL", {"period_sec": periodsec})
|
|
706
|
+
try:
|
|
707
|
+
await self.execute("calc-dirty-rate", {"calc-time": periodsec, "mode": mode})
|
|
708
|
+
await asyncio.sleep(periodsec + 0.3)
|
|
709
|
+
data = await self.execute("query-dirty-rate")
|
|
710
|
+
rate = data.get("dirty-rate", 0) if isinstance(data, dict) else 0
|
|
711
|
+
return {
|
|
712
|
+
"mode": mode,
|
|
713
|
+
"rate_mb_s": rate / (1024 * 1024) if isinstance(rate, int) else data,
|
|
714
|
+
"raw": data,
|
|
715
|
+
}
|
|
716
|
+
except QemuBridgeError as e:
|
|
717
|
+
logger.warning("calc-dirty-rate not supported: %s (%s)", e, e.code)
|
|
718
|
+
return {"mode": "unsupported", "dirty-rate": -1}
|
|
719
|
+
|
|
720
|
+
# ----------------------------------------------------------------------- #
|
|
721
|
+
# context 19 - block jobs
|
|
722
|
+
# ----------------------------------------------------------------------- #
|
|
723
|
+
|
|
724
|
+
async def queryblockjobs(self) -> list[dict[str, Any]]:
|
|
725
|
+
"""Op 18/25 - lists active block jobs with unified query-jobs fallback."""
|
|
726
|
+
try:
|
|
727
|
+
return await self.execute("query-block-jobs")
|
|
728
|
+
except QemuBridgeError as e:
|
|
729
|
+
logger.warning("query-block-jobs failed (%s) - query-jobs fallback", e.code)
|
|
730
|
+
return await self.execute("query-jobs")
|
|
731
|
+
|
|
732
|
+
async def blockjobstart(
|
|
733
|
+
self,
|
|
734
|
+
device: str = "drive0",
|
|
735
|
+
target: str = "/var/lib/vhe/backup.qcow2",
|
|
736
|
+
sync: str = "full",
|
|
737
|
+
) -> dict[str, Any]:
|
|
738
|
+
"""Op 19/25 - starts a mirror job via blockdev-mirror with drive-mirror fallback."""
|
|
739
|
+
jobid = f"job-{device}-{int(time.time()) % 100000}"
|
|
740
|
+
try:
|
|
741
|
+
result = await self.execute("blockdev-mirror", {
|
|
742
|
+
"job-id": jobid,
|
|
743
|
+
"device": device,
|
|
744
|
+
"target": target,
|
|
745
|
+
"sync": sync,
|
|
746
|
+
})
|
|
747
|
+
except QemuBridgeError as e:
|
|
748
|
+
logger.warning("blockdev-mirror failed (%s) - drive-mirror fallback", e.code)
|
|
749
|
+
result = await self.execute("drive-mirror", {
|
|
750
|
+
"device": device,
|
|
751
|
+
"target": target,
|
|
752
|
+
"sync": sync,
|
|
753
|
+
"job-id": jobid,
|
|
754
|
+
})
|
|
755
|
+
return {"job_id": jobid, "device": device, "target": target, "result": result}
|
|
756
|
+
|
|
757
|
+
async def blockjobcancel(self, deviceorjob: str, force: bool = False) -> dict[str, Any]:
|
|
758
|
+
"""Op 20/25 - cancels a block job by device or job-id, both orders."""
|
|
759
|
+
try:
|
|
760
|
+
result = await self.execute("block-job-cancel", {"device": deviceorjob, "force": force})
|
|
761
|
+
except QemuBridgeError as e:
|
|
762
|
+
logger.warning("block-job-cancel failed (%s) - job-cancel fallback", e.code)
|
|
763
|
+
result = await self.execute("job-cancel", {"id": deviceorjob, "force": force})
|
|
764
|
+
return {"cancelled": deviceorjob, "result": result}
|
|
765
|
+
|
|
766
|
+
# ----------------------------------------------------------------------- #
|
|
767
|
+
# context 20 - vcpu affinity and io throttling
|
|
768
|
+
# ----------------------------------------------------------------------- #
|
|
769
|
+
|
|
770
|
+
async def setvcpuaffinity(self, vcpuid: int = 0, cpuset: str = "0-1") -> dict[str, Any]:
|
|
771
|
+
"""
|
|
772
|
+
Op 21/25 - pins a vCPU thread trying three strategies in order:
|
|
773
|
+
os.sched_setaffinity, then taskset -pc via asyncio subprocess, then
|
|
774
|
+
a logged mock when /proc is absent (CI sandboxes).
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
vcpuid: Guest vCPU index as reported by query-cpus-fast.
|
|
778
|
+
cpuset: Linux cpuset expression such as ``0,2-3,5``.
|
|
779
|
+
|
|
780
|
+
Returns:
|
|
781
|
+
dict: Pinning result with the method used and mock flag.
|
|
782
|
+
"""
|
|
783
|
+
cpus = await self.querycpus()
|
|
784
|
+
threadid: int | None = None
|
|
785
|
+
for c in cpus:
|
|
786
|
+
if c.get("cpu-index") == vcpuid or c.get("CPU") == vcpuid:
|
|
787
|
+
threadid = c.get("thread-id") or c.get("thread_id")
|
|
788
|
+
break
|
|
789
|
+
hostlist = parsecpuset(cpuset)
|
|
790
|
+
if threadid:
|
|
791
|
+
try:
|
|
792
|
+
os.sched_setaffinity(threadid, set(hostlist))
|
|
793
|
+
return {"vcpu": vcpuid, "thread_id": threadid, "cpuset": cpuset, "pinned": True, "method": "sched_setaffinity"}
|
|
794
|
+
except (AttributeError, OSError) as e:
|
|
795
|
+
logger.warning("sched_setaffinity failed for tid %s: %s - taskset fallback", threadid, e)
|
|
796
|
+
if threadid and os.path.exists(f"/proc/{threadid}"):
|
|
797
|
+
try:
|
|
798
|
+
proc = await asyncio.create_subprocess_exec(
|
|
799
|
+
"taskset", "-pc", cpuset, str(threadid),
|
|
800
|
+
stdout=asyncio.subprocess.PIPE,
|
|
801
|
+
stderr=asyncio.subprocess.PIPE,
|
|
802
|
+
)
|
|
803
|
+
stdout, stderr = await proc.communicate()
|
|
804
|
+
return {
|
|
805
|
+
"vcpu": vcpuid,
|
|
806
|
+
"thread_id": threadid,
|
|
807
|
+
"cpuset": cpuset,
|
|
808
|
+
"pinned": proc.returncode == 0,
|
|
809
|
+
"method": "taskset",
|
|
810
|
+
"stdout": stdout.decode(),
|
|
811
|
+
"stderr": stderr.decode(),
|
|
812
|
+
"returncode": proc.returncode,
|
|
813
|
+
}
|
|
814
|
+
except Exception as e:
|
|
815
|
+
raise QemuBridgeError(f"affinity set failed {e}", "AFFINITY_FAILED") from e
|
|
816
|
+
logger.info("sim affinity vcpu %s -> %s (mock: no /proc/%s)", vcpuid, cpuset, threadid)
|
|
817
|
+
return {"vcpu": vcpuid, "thread_id": threadid or 10000 + vcpuid, "cpuset": cpuset, "pinned": False, "mock": True}
|
|
818
|
+
|
|
819
|
+
async def throttleio(
|
|
820
|
+
self,
|
|
821
|
+
device: str = "drive0",
|
|
822
|
+
bps: int = 104857600,
|
|
823
|
+
iops: int = 1000,
|
|
824
|
+
bpsrd: int = 0,
|
|
825
|
+
bpswr: int | None = None,
|
|
826
|
+
iopsrd: int = 0,
|
|
827
|
+
iopswr: int | None = None,
|
|
828
|
+
) -> dict[str, Any]:
|
|
829
|
+
"""
|
|
830
|
+
Op 22/25 - throttles a block device (0 keeps the axis unlimited).
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
device: Block device id, for example ``drive0``.
|
|
834
|
+
bps: Total byte cap; write cap defaults to the same value.
|
|
835
|
+
iops: Total io cap; write cap defaults to the same value.
|
|
836
|
+
bpsrd: Read byte cap; 0 keeps the read axis unlimited.
|
|
837
|
+
bpswr: Optional write byte cap overriding ``bps``.
|
|
838
|
+
iopsrd: Read io cap; 0 keeps the read axis unlimited.
|
|
839
|
+
iopswr: Optional write io cap overriding ``iops``.
|
|
840
|
+
|
|
841
|
+
Returns:
|
|
842
|
+
dict: The applied device, bps, iops and raw QMP result.
|
|
843
|
+
"""
|
|
844
|
+
args: dict[str, Any] = {
|
|
845
|
+
"device": device,
|
|
846
|
+
"bps": bps,
|
|
847
|
+
"bps_rd": bpsrd,
|
|
848
|
+
"bps_wr": bps if bpswr is None else bpswr,
|
|
849
|
+
"iops": iops,
|
|
850
|
+
"iops_rd": iopsrd,
|
|
851
|
+
"iops_wr": iops if iopswr is None else iopswr,
|
|
852
|
+
}
|
|
853
|
+
result = await self.execute("block_set_io_throttle", args)
|
|
854
|
+
return {"device": device, "bps": bps, "iops": iops, "result": result}
|
|
855
|
+
|
|
856
|
+
# ----------------------------------------------------------------------- #
|
|
857
|
+
# context 21 - version introspection
|
|
858
|
+
# ----------------------------------------------------------------------- #
|
|
859
|
+
|
|
860
|
+
async def qemuversion(self) -> dict[str, Any]:
|
|
861
|
+
"""Op 23/25 - reports QEMU version, features and command count."""
|
|
862
|
+
version = await self.execute("query-version")
|
|
863
|
+
if not isinstance(version, dict):
|
|
864
|
+
version = {}
|
|
865
|
+
try:
|
|
866
|
+
features = await self.execute("query-qemu-features")
|
|
867
|
+
except QemuBridgeError as e:
|
|
868
|
+
logger.debug("query-qemu-features unavailable: %s", e.code)
|
|
869
|
+
features = []
|
|
870
|
+
try:
|
|
871
|
+
commands = await self.execute("query-commands")
|
|
872
|
+
except QemuBridgeError as e:
|
|
873
|
+
logger.debug("query-commands unavailable: %s", e.code)
|
|
874
|
+
commands = []
|
|
875
|
+
return {
|
|
876
|
+
"qemu": version.get("qemu", {}),
|
|
877
|
+
"package": version.get("package", ""),
|
|
878
|
+
"features": features,
|
|
879
|
+
"command_count": len(commands),
|
|
880
|
+
"target": QEMUVERSIONTARGET,
|
|
881
|
+
"python": PYTHONVERSIONTARGET,
|
|
882
|
+
"node": NODECURRENTVERSION,
|
|
883
|
+
"driver": DRIVERVERSIONTARGET,
|
|
884
|
+
"cuda": CUDAVERSIONTARGET,
|
|
885
|
+
"bridge": BRIDGEVERSION,
|
|
886
|
+
"date": BUILDDATE,
|
|
887
|
+
"compat": self.getnodecompat(),
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def parsecpuset(cpuset: str) -> list[int]:
|
|
892
|
+
"""
|
|
893
|
+
Parses a linux cpuset expression such as '0,2-3,5' into a cpu list.
|
|
894
|
+
|
|
895
|
+
Args:
|
|
896
|
+
cpuset: Comma separated indices and closed ranges with hyphens.
|
|
897
|
+
|
|
898
|
+
Returns:
|
|
899
|
+
list[int]: The expanded list of cpu indices in ascending visit order.
|
|
900
|
+
|
|
901
|
+
Raises:
|
|
902
|
+
QemuBridgeError: When the expression is invalid or expands to empty.
|
|
903
|
+
"""
|
|
904
|
+
out: list[int] = []
|
|
905
|
+
try:
|
|
906
|
+
for part in cpuset.split(","):
|
|
907
|
+
part = part.strip()
|
|
908
|
+
if not part:
|
|
909
|
+
continue
|
|
910
|
+
lo, sep, hi = part.partition("-")
|
|
911
|
+
if sep:
|
|
912
|
+
out.extend(range(int(lo), int(hi) + 1))
|
|
913
|
+
else:
|
|
914
|
+
out.append(int(part))
|
|
915
|
+
except ValueError as e:
|
|
916
|
+
raise QemuBridgeError(f"invalid cpuset {cpuset!r}", "EINVAL") from e
|
|
917
|
+
if not out:
|
|
918
|
+
raise QemuBridgeError(f"empty cpuset {cpuset!r}", "EINVAL")
|
|
919
|
+
return out
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
# --------------------------------------------------------------------------- #
|
|
923
|
+
# context 22 - session helpers (qmpsession, batch hotplug)
|
|
924
|
+
# --------------------------------------------------------------------------- #
|
|
925
|
+
|
|
926
|
+
@asynccontextmanager
|
|
927
|
+
async def qmpsession(socketpath: str = "/run/vhe/vm.qmp", timeout: float = QEMUDEFAULTTIMEOUT):
|
|
928
|
+
"""
|
|
929
|
+
Yields a connected QemuBridge and always disconnects on exit.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
socketpath: QMP unix socket path to connect to.
|
|
933
|
+
timeout: Per-command timeout in seconds for every exchange.
|
|
934
|
+
|
|
935
|
+
Yields:
|
|
936
|
+
QemuBridge: A connected bridge with capabilities negotiated best effort.
|
|
937
|
+
"""
|
|
938
|
+
bridge = QemuBridge(socketpath=socketpath, timeout=timeout)
|
|
939
|
+
await bridge.connect()
|
|
940
|
+
try:
|
|
941
|
+
await bridge.handshake()
|
|
942
|
+
except Exception:
|
|
943
|
+
logger.warning("handshake deferred inside qmpsession - CI mock mode")
|
|
944
|
+
try:
|
|
945
|
+
yield bridge
|
|
946
|
+
finally:
|
|
947
|
+
await bridge.disconnect()
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
async def batchhotplugfromconfig(vmid: str, configpath: str = "vcpus.json") -> dict[str, Any]:
|
|
951
|
+
"""
|
|
952
|
+
Reads vcpus.json and hotplugs CPUs until the desired count is met.
|
|
953
|
+
|
|
954
|
+
Args:
|
|
955
|
+
vmid: VM identifier used to resolve the per-vm QMP socket.
|
|
956
|
+
configpath: Path of the json file holding the desired vCPU count.
|
|
957
|
+
|
|
958
|
+
Returns:
|
|
959
|
+
dict: The requested count plus one result entry per hotplugged core.
|
|
960
|
+
"""
|
|
961
|
+
p = Path(configpath)
|
|
962
|
+
if not p.exists():
|
|
963
|
+
return {"error": f"{configpath} not found"}
|
|
964
|
+
try:
|
|
965
|
+
desired = json.loads(p.read_text())
|
|
966
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
967
|
+
logger.warning("failed to read %s: %s", configpath, e)
|
|
968
|
+
return {"error": str(e)}
|
|
969
|
+
count = desired.get(vmid, desired.get("count", 4)) if isinstance(desired, dict) else 4
|
|
970
|
+
socketpath = QEMUDEFAULTSOCKET.format(vmid=vmid)
|
|
971
|
+
results: list[dict[str, Any]] = []
|
|
972
|
+
try:
|
|
973
|
+
async with qmpsession(socketpath) as bridge:
|
|
974
|
+
current = await bridge.querycpus()
|
|
975
|
+
gap = int(count) - len(current)
|
|
976
|
+
for i in range(max(gap, 0)):
|
|
977
|
+
try:
|
|
978
|
+
results.append(await bridge.hotplugcpu(socketid=0, coreid=len(current) + i))
|
|
979
|
+
except QemuBridgeError as e:
|
|
980
|
+
logger.warning("batch hotplug core %s failed: %s (%s)", len(current) + i, e, e.code)
|
|
981
|
+
results.append({"error": str(e), "code": e.code})
|
|
982
|
+
except QemuBridgeError as e:
|
|
983
|
+
logger.error("batch hotplug session failed: %s (%s)", e, e.code)
|
|
984
|
+
return {"error": str(e), "code": e.code, "results": results}
|
|
985
|
+
return {"requested": count, "results": results}
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
# --------------------------------------------------------------------------- #
|
|
989
|
+
# context 23 - flat project configs (14 file union)
|
|
990
|
+
# --------------------------------------------------------------------------- #
|
|
991
|
+
|
|
992
|
+
FLATCONFIGFILES: tuple[str, ...] = (
|
|
993
|
+
"vm.config", "gpu.config", "passage.config", "qemu.config", "mttg.config", "docker.config",
|
|
994
|
+
"boards.json", "cores.json", "processors.json", "gpus.json", "vcpus.json", "vram.json",
|
|
995
|
+
"specs_amd_ryzen_x_series.json", "specs_nvidia_blackwell.json",
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def loadallconfigs(basedir: str = ".") -> dict[str, Any]:
|
|
1000
|
+
"""
|
|
1001
|
+
Loads the 14 flat project configs (6 kv + 8 json) with per-file defense.
|
|
1002
|
+
|
|
1003
|
+
Args:
|
|
1004
|
+
basedir: Base directory holding the flat config family.
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
dict: One entry per file with parsed data or a missing/error marker.
|
|
1008
|
+
"""
|
|
1009
|
+
cfg: dict[str, Any] = {}
|
|
1010
|
+
for fname in FLATCONFIGFILES:
|
|
1011
|
+
fpath = Path(basedir) / fname
|
|
1012
|
+
if not fpath.exists():
|
|
1013
|
+
cfg[fname] = {"missing": True, "note": f"{fname} will be created by other subagents"}
|
|
1014
|
+
continue
|
|
1015
|
+
try:
|
|
1016
|
+
if fpath.suffix == ".json":
|
|
1017
|
+
cfg[fname] = json.loads(fpath.read_text())
|
|
1018
|
+
else:
|
|
1019
|
+
parsed: dict[str, str] = {}
|
|
1020
|
+
for line in fpath.read_text().splitlines():
|
|
1021
|
+
line = line.strip()
|
|
1022
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
1023
|
+
continue
|
|
1024
|
+
k, _, v = line.partition("=")
|
|
1025
|
+
parsed[k.strip()] = v.strip().strip('"').strip("'")
|
|
1026
|
+
cfg[fname] = parsed
|
|
1027
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
1028
|
+
logger.warning("config %s failed to parse: %s", fname, e)
|
|
1029
|
+
cfg[fname] = {"error": str(e)}
|
|
1030
|
+
return cfg
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
# --------------------------------------------------------------------------- #
|
|
1034
|
+
# context 24 - smiadapter internal module (a100/h100 flex nvidia-smi facade)
|
|
1035
|
+
# --------------------------------------------------------------------------- #
|
|
1036
|
+
|
|
1037
|
+
class smiadapter:
|
|
1038
|
+
"""
|
|
1039
|
+
Renders a software nvidia-smi facade with flexible a100/h100 profiles.
|
|
1040
|
+
|
|
1041
|
+
The class is the python-side twin of the gpumonitor.cpp renderer and
|
|
1042
|
+
keeps the nvidia-smi field vocabulary (memory.total, utilization.gpu,
|
|
1043
|
+
...) as external API conventions. Driver is re-pinned to 575.57.08 with
|
|
1044
|
+
CUDA 12.9.
|
|
1045
|
+
"""
|
|
1046
|
+
|
|
1047
|
+
profiles: dict[str, dict[str, str]] = {
|
|
1048
|
+
"h100": {
|
|
1049
|
+
"name": "NVIDIA H100 80GB HBM3", "mem": "81559MiB", "used": "1024MiB",
|
|
1050
|
+
"util": "0%", "temp": "35C", "power": "120W / 700W", "bus": "00000000:00:04.0",
|
|
1051
|
+
},
|
|
1052
|
+
"a100": {
|
|
1053
|
+
"name": "NVIDIA A100-SXM4-40GB", "mem": "40536MiB", "used": "512MiB",
|
|
1054
|
+
"util": "0%", "temp": "32C", "power": "85W / 400W", "bus": "00000000:00:05.0",
|
|
1055
|
+
},
|
|
1056
|
+
"rtx5090": {
|
|
1057
|
+
"name": "NVIDIA GeForce RTX 5090", "mem": "32768MiB", "used": "256MiB",
|
|
1058
|
+
"util": "0%", "temp": "38C", "power": "90W / 575W", "bus": "00000000:00:06.0",
|
|
1059
|
+
},
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
@classmethod
|
|
1063
|
+
def select(cls, gpus: list[str] | None) -> list[tuple[str, dict[str, str]]]:
|
|
1064
|
+
"""Selects flex profiles defensively, skipping unknown keys."""
|
|
1065
|
+
chosen: list[tuple[str, dict[str, str]]] = []
|
|
1066
|
+
for key in gpus or ["a100", "h100"]:
|
|
1067
|
+
profile = cls.profiles.get(key)
|
|
1068
|
+
if profile is None:
|
|
1069
|
+
logger.warning("unknown smiadapter profile %r - skipping", key)
|
|
1070
|
+
continue
|
|
1071
|
+
chosen.append((key, profile))
|
|
1072
|
+
return chosen
|
|
1073
|
+
|
|
1074
|
+
@classmethod
|
|
1075
|
+
def renderlines(cls, gpus: list[str] | None = None) -> list[str]:
|
|
1076
|
+
"""Renders the classic bordered smi table for the chosen profiles."""
|
|
1077
|
+
chosen = cls.select(gpus)
|
|
1078
|
+
width = 79
|
|
1079
|
+
close = lambda s: s.ljust(width - 1) + "|" # noqa: E731 - local row closer
|
|
1080
|
+
lines = [
|
|
1081
|
+
"+" + "-" * (width - 2) + "+",
|
|
1082
|
+
close(f"| NVIDIA-SMI {DRIVERVERSIONTARGET} Driver Version: {DRIVERVERSIONTARGET} CUDA Version: {CUDAVERSIONTARGET}"),
|
|
1083
|
+
"+-----------------------------------------+-----------------+-----------------+",
|
|
1084
|
+
"| GPU Name Persist.-M | Bus-Id Disp.A |",
|
|
1085
|
+
]
|
|
1086
|
+
for i, (_, g) in enumerate(chosen):
|
|
1087
|
+
lines.append(close(f"| {i} {g['name'][:32]:<32} On | {g['bus']} Off"))
|
|
1088
|
+
lines.append("|=========================================+=================+=================|")
|
|
1089
|
+
for i, (_, g) in enumerate(chosen):
|
|
1090
|
+
lines.append(close(f"| {i} {g['name'][:20]:<20} {g['temp']:<5} {g['power']:<15} | {g['used']:>9} / {g['mem']:<9} {g['util']:>4} Default"))
|
|
1091
|
+
lines.append("+" + "-" * (width - 2) + "+")
|
|
1092
|
+
return lines
|
|
1093
|
+
|
|
1094
|
+
@classmethod
|
|
1095
|
+
def renderlist(cls, gpus: list[str] | None = None) -> list[str]:
|
|
1096
|
+
"""Renders the nvidia-smi -L device listing format."""
|
|
1097
|
+
return [
|
|
1098
|
+
f"GPU {i}: {g['name']} (UUID: GPU-fake-{key}-{i})"
|
|
1099
|
+
for i, (key, g) in enumerate(cls.select(gpus))
|
|
1100
|
+
]
|
|
1101
|
+
|
|
1102
|
+
@classmethod
|
|
1103
|
+
def renderquery(cls, gpus: list[str] | None, fields: str) -> str:
|
|
1104
|
+
"""Renders the nvidia-smi --query-gpu csv subset for the profiles."""
|
|
1105
|
+
wanted = [f.strip() for f in fields.split(",") if f.strip()]
|
|
1106
|
+
mapping: dict[str, dict[str, str]] = {}
|
|
1107
|
+
for key, g in cls.select(gpus):
|
|
1108
|
+
mapping[key] = {
|
|
1109
|
+
"name": g["name"],
|
|
1110
|
+
"memory.total": g["mem"],
|
|
1111
|
+
"memory.used": g["used"],
|
|
1112
|
+
"utilization.gpu": g["util"].rstrip("%"),
|
|
1113
|
+
"temperature.gpu": g["temp"].rstrip("C"),
|
|
1114
|
+
"power.draw": g["power"].split(" / ")[0].rstrip("W"),
|
|
1115
|
+
"driver_version": DRIVERVERSIONTARGET,
|
|
1116
|
+
}
|
|
1117
|
+
rows = [",".join(wanted)]
|
|
1118
|
+
for key, values in mapping.items():
|
|
1119
|
+
row = [str(values.get(f, "N/A")) for f in wanted]
|
|
1120
|
+
if any(v == "N/A" for v in row):
|
|
1121
|
+
logger.debug("smiadapter query has N/A fields for %s", key)
|
|
1122
|
+
rows.append(",".join(row))
|
|
1123
|
+
return "\n".join(rows)
|
|
1124
|
+
|
|
1125
|
+
|
|
1126
|
+
# --------------------------------------------------------------------------- #
|
|
1127
|
+
# context 25 - node shim generator, cli modes and offline selftest
|
|
1128
|
+
# --------------------------------------------------------------------------- #
|
|
1129
|
+
|
|
1130
|
+
NODESHIMTEMPLATE = """/**
|
|
1131
|
+
* qemubridge.node.js - node compat shim generated by qemubridge.py __BRIDGE__
|
|
1132
|
+
* date __DATE__ - qemu __QEMU__ - python __PY__+
|
|
1133
|
+
* node __NODECUR__ current / __NODELTS__ lts - node:net ndjson over unix QMP.
|
|
1134
|
+
*/
|
|
1135
|
+
import net from 'node:net';
|
|
1136
|
+
|
|
1137
|
+
export const QEMU_VERSION = '__QEMU__';
|
|
1138
|
+
export const BRIDGEVERSION = '__BRIDGE__';
|
|
1139
|
+
|
|
1140
|
+
export class NodeQemuBridge {
|
|
1141
|
+
constructor(socketPath = '__SOCKET__') {
|
|
1142
|
+
this.socketPath = socketPath;
|
|
1143
|
+
this.client = null;
|
|
1144
|
+
this.buf = '';
|
|
1145
|
+
}
|
|
1146
|
+
connect() {
|
|
1147
|
+
return new Promise((res, rej) => {
|
|
1148
|
+
this.client = net.createConnection(this.socketPath, () => res(true));
|
|
1149
|
+
this.client.on('error', rej);
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
send(cmd) {
|
|
1153
|
+
return new Promise((res, rej) => {
|
|
1154
|
+
this.client.write(JSON.stringify(cmd) + '\\n');
|
|
1155
|
+
this.client.on('data', (d) => {
|
|
1156
|
+
this.buf += d.toString();
|
|
1157
|
+
const i = this.buf.indexOf('\\n');
|
|
1158
|
+
if (i < 0) return;
|
|
1159
|
+
const line = this.buf.slice(0, i);
|
|
1160
|
+
this.buf = this.buf.slice(i + 1);
|
|
1161
|
+
let resp;
|
|
1162
|
+
try { resp = JSON.parse(line); } catch (e) { rej(e); return; }
|
|
1163
|
+
if (resp.event) return;
|
|
1164
|
+
if (resp.error) { rej(new Error(resp.error.desc)); return; }
|
|
1165
|
+
res(resp.return !== undefined ? resp.return : resp);
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
async handshake() {
|
|
1170
|
+
await this.send({ execute: 'qmp_capabilities', arguments: { enable: ['oob'] } });
|
|
1171
|
+
return this.send({ execute: 'query-version' });
|
|
1172
|
+
}
|
|
1173
|
+
queryStatus() { return this.send({ execute: 'query-status' }); }
|
|
1174
|
+
qemuVersion() { return this.send({ execute: 'query-version' }); }
|
|
1175
|
+
}
|
|
1176
|
+
"""
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def generatenodeshimjs() -> str:
|
|
1180
|
+
"""
|
|
1181
|
+
Returns the node compat shim source with pinned v2 versions.
|
|
1182
|
+
|
|
1183
|
+
Returns:
|
|
1184
|
+
str: The rendered node:net ndjson QMP shim as javascript source.
|
|
1185
|
+
"""
|
|
1186
|
+
return (
|
|
1187
|
+
NODESHIMTEMPLATE
|
|
1188
|
+
.replace("__BRIDGE__", BRIDGEVERSION)
|
|
1189
|
+
.replace("__DATE__", BUILDDATE)
|
|
1190
|
+
.replace("__QEMU__", QEMUVERSIONTARGET)
|
|
1191
|
+
.replace("__PY__", PYTHONVERSIONTARGET)
|
|
1192
|
+
.replace("__NODECUR__", NODECURRENTVERSION)
|
|
1193
|
+
.replace("__NODELTS__", NODELTSVERSION)
|
|
1194
|
+
.replace("__SOCKET__", "/run/vhe/vm.qmp")
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def buildcli() -> argparse.ArgumentParser:
|
|
1199
|
+
"""Builds the standalone argument parser with all bridge modes."""
|
|
1200
|
+
parser = argparse.ArgumentParser(
|
|
1201
|
+
prog="qemubridge.py",
|
|
1202
|
+
description="QMP bridge for the virtual hardware engine v2 (qemu 11.1.0, python 3.14+)",
|
|
1203
|
+
)
|
|
1204
|
+
parser.add_argument("--socket", default="/run/vhe/vm.qmp", help="QMP unix socket path (default /run/vhe/vm.qmp)")
|
|
1205
|
+
parser.add_argument("--timeout", type=float, default=10.0, help="per-command timeout in seconds")
|
|
1206
|
+
parser.add_argument("--verbose", action="store_true", help="debug logging")
|
|
1207
|
+
sub = parser.add_subparsers(dest="mode")
|
|
1208
|
+
sub.add_parser("selftest", help="offline selftest (default when no mode is given)")
|
|
1209
|
+
sub.add_parser("status", help="query vm runstate (typed VMStatus)")
|
|
1210
|
+
sub.add_parser("cpus", help="list vCPUs (query-cpus-fast)")
|
|
1211
|
+
sub.add_parser("memory", help="memory summary, devices and balloon")
|
|
1212
|
+
sub.add_parser("version", help="qemu version, features and command count")
|
|
1213
|
+
pmerge = sub.add_parser("migrate", help="live migration to an explicit target")
|
|
1214
|
+
pmerge.add_argument("--uri", default=None, help="full migration uri e.g. tcp:host:port")
|
|
1215
|
+
pmerge.add_argument("--host", default=None, help="target host (or VHE_MIGRATE_HOST; never implicit localhost)")
|
|
1216
|
+
pmerge.add_argument("--port", type=int, default=None, help="target port (default random 30000-59999)")
|
|
1217
|
+
pmerge.add_argument("--channels", type=int, default=8, help="multifd channels 2-32")
|
|
1218
|
+
pmerge.add_argument("--downtime", type=int, default=300, help="downtime limit ms")
|
|
1219
|
+
pmerge.add_argument("--no-wait", dest="nowait", action="store_true", help="launch without polling to completion")
|
|
1220
|
+
sub.add_parser("blockjobs", help="list active block jobs")
|
|
1221
|
+
pbatch = sub.add_parser("batchcpus", help="batch vCPU hotplug from vcpus.json")
|
|
1222
|
+
pbatch.add_argument("--vmid", default="vm0", help="vm id for socket resolution")
|
|
1223
|
+
pbatch.add_argument("--config", default="vcpus.json", help="desired count config path")
|
|
1224
|
+
pconf = sub.add_parser("configs", help="load the 14 flat project configs")
|
|
1225
|
+
pconf.add_argument("--base", default=".", help="base directory of the flat configs")
|
|
1226
|
+
psmi = sub.add_parser("smiadapter", help="software nvidia-smi facade (a100/h100 flex)")
|
|
1227
|
+
psmi.add_argument("--gpu", action="append", choices=["a100", "h100", "rtx5090"], help="profile to render (repeatable)")
|
|
1228
|
+
psmi.add_argument("--list", action="store_true", help="nvidia-smi -L listing format")
|
|
1229
|
+
psmi.add_argument("--query", default=None, help="--query-gpu csv fields e.g. name,memory.total")
|
|
1230
|
+
sub.add_parser("shim", help="print the node compat shim source")
|
|
1231
|
+
return parser
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
async def runqmpmode(args: argparse.Namespace) -> int:
|
|
1235
|
+
"""Runs one QMP-backed cli mode inside a managed session."""
|
|
1236
|
+
try:
|
|
1237
|
+
async with qmpsession(args.socket, args.timeout) as bridge:
|
|
1238
|
+
match args.mode:
|
|
1239
|
+
case "status":
|
|
1240
|
+
print((await bridge.querystatus()).value)
|
|
1241
|
+
case "cpus":
|
|
1242
|
+
print(json.dumps(await bridge.querycpus(), indent=2))
|
|
1243
|
+
case "memory":
|
|
1244
|
+
print(json.dumps(await bridge.querymemory(), indent=2))
|
|
1245
|
+
case "version":
|
|
1246
|
+
print(json.dumps(await bridge.qemuversion(), indent=2, default=str))
|
|
1247
|
+
case "blockjobs":
|
|
1248
|
+
print(json.dumps(await bridge.queryblockjobs(), indent=2))
|
|
1249
|
+
case "batchcpus":
|
|
1250
|
+
print(json.dumps(await batchhotplugfromconfig(args.vmid, args.config), indent=2, default=str))
|
|
1251
|
+
case "migrate":
|
|
1252
|
+
stats = await bridge.livemigrate(
|
|
1253
|
+
uri=args.uri,
|
|
1254
|
+
host=args.host,
|
|
1255
|
+
port=args.port,
|
|
1256
|
+
multifdchannels=args.channels,
|
|
1257
|
+
downtimems=args.downtime,
|
|
1258
|
+
wait=not args.nowait,
|
|
1259
|
+
)
|
|
1260
|
+
print(json.dumps(asdict(stats), indent=2))
|
|
1261
|
+
case _:
|
|
1262
|
+
logger.error("unhandled qmp mode %s", args.mode)
|
|
1263
|
+
return 2
|
|
1264
|
+
except QemuBridgeError as e:
|
|
1265
|
+
logger.error("mode %s failed: %s (%s)", args.mode, e, e.code)
|
|
1266
|
+
return 1
|
|
1267
|
+
return 0
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
def runcli(args: argparse.Namespace) -> int:
|
|
1271
|
+
"""Dispatches one cli mode; returns the process exit code."""
|
|
1272
|
+
match args.mode:
|
|
1273
|
+
case "selftest" | None:
|
|
1274
|
+
asyncio.run(selftest())
|
|
1275
|
+
return 0
|
|
1276
|
+
case "smiadapter":
|
|
1277
|
+
if args.list:
|
|
1278
|
+
print("\n".join(smiadapter.renderlist(args.gpu)))
|
|
1279
|
+
elif args.query:
|
|
1280
|
+
print(smiadapter.renderquery(args.gpu, args.query))
|
|
1281
|
+
else:
|
|
1282
|
+
print("\n".join(smiadapter.renderlines(args.gpu)))
|
|
1283
|
+
return 0
|
|
1284
|
+
case "shim":
|
|
1285
|
+
print(generatenodeshimjs())
|
|
1286
|
+
return 0
|
|
1287
|
+
case "configs":
|
|
1288
|
+
cfg = loadallconfigs(args.base)
|
|
1289
|
+
for name, value in cfg.items():
|
|
1290
|
+
if isinstance(value, dict) and value.get("missing"):
|
|
1291
|
+
state = "missing"
|
|
1292
|
+
elif isinstance(value, dict) and value.get("error"):
|
|
1293
|
+
state = f"error: {value['error']}"
|
|
1294
|
+
else:
|
|
1295
|
+
state = f"ok ({len(value)} keys)" if isinstance(value, dict) else "ok"
|
|
1296
|
+
print(f"{name:<32} {state}")
|
|
1297
|
+
return 0
|
|
1298
|
+
case "status" | "cpus" | "memory" | "version" | "migrate" | "blockjobs" | "batchcpus":
|
|
1299
|
+
return asyncio.run(runqmpmode(args))
|
|
1300
|
+
case _:
|
|
1301
|
+
logger.error("unknown mode %s", args.mode)
|
|
1302
|
+
return 2
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
async def selftest() -> None:
|
|
1306
|
+
"""Runs the offline selftest covering models, smiadapter, shim and mock link."""
|
|
1307
|
+
bridge = QemuBridge(socketpath="/nonexistent/vm.qmp")
|
|
1308
|
+
assert await bridge.connect() is True # mock CI path, logged not raised
|
|
1309
|
+
compat = bridge.getnodecompat()
|
|
1310
|
+
assert compat["qemu"] == QEMUVERSIONTARGET
|
|
1311
|
+
assert VMStatus("inmigrate") is VMStatus.INMIGRATE
|
|
1312
|
+
cfg = QEMUConfig.loadfromfile("/nonexistent/qemu.config", vmid="t0")
|
|
1313
|
+
assert "vm-t0" in cfg.socketpath and cfg.hugepages is False
|
|
1314
|
+
assert parsecpuset("0,2-3,5") == [0, 2, 3, 5]
|
|
1315
|
+
table = smiadapter.renderlines(["a100", "h100"])
|
|
1316
|
+
assert any("81559MiB" in line for line in table) and any("40536MiB" in line for line in table)
|
|
1317
|
+
assert "575.57.08" in table[1] and CUDAVERSIONTARGET in table[1]
|
|
1318
|
+
listing = smiadapter.renderlist(["h100"])
|
|
1319
|
+
assert listing and "GPU-fake-h100" in listing[0]
|
|
1320
|
+
csv = smiadapter.renderquery(["a100"], "name,memory.total,driver_version")
|
|
1321
|
+
assert csv.splitlines()[1].endswith(DRIVERVERSIONTARGET)
|
|
1322
|
+
shim = generatenodeshimjs()
|
|
1323
|
+
assert QEMUVERSIONTARGET in shim and NODECURRENTVERSION in shim
|
|
1324
|
+
assert "localhost" not in shim and "127.0.0.1" not in shim and "0.0.0.0" not in shim
|
|
1325
|
+
cfgs = loadallconfigs("/nonexistent/dir")
|
|
1326
|
+
assert len(cfgs) == len(FLATCONFIGFILES) and cfgs["vm.config"]["missing"] is True
|
|
1327
|
+
await bridge.disconnect()
|
|
1328
|
+
print(
|
|
1329
|
+
f"qemubridge.py {BRIDGEVERSION} selftest ok - qemu {QEMUVERSIONTARGET} "
|
|
1330
|
+
f"node {NODECURRENTVERSION}/{NODELTSVERSION} python {PYTHONVERSIONTARGET}+ ({BUILDDATE})"
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
if __name__ == "__main__":
|
|
1335
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [qemubridge] %(levelname)s %(message)s")
|
|
1336
|
+
cliargs = buildcli().parse_args()
|
|
1337
|
+
if cliargs.verbose:
|
|
1338
|
+
logger.setLevel(logging.DEBUG)
|
|
1339
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
1340
|
+
sys.exit(runcli(cliargs))
|