dodo-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +425 -0
- package/dist/auth/adapter.js +43 -0
- package/dist/auth/adapter.js.map +1 -0
- package/dist/auth/clientDeletion.js +45 -0
- package/dist/auth/clientDeletion.js.map +1 -0
- package/dist/auth/clients.js +55 -0
- package/dist/auth/clients.js.map +1 -0
- package/dist/auth/constants.js +10 -0
- package/dist/auth/constants.js.map +1 -0
- package/dist/auth/interactions.js +184 -0
- package/dist/auth/interactions.js.map +1 -0
- package/dist/auth/keys.js +56 -0
- package/dist/auth/keys.js.map +1 -0
- package/dist/auth/provider.js +131 -0
- package/dist/auth/provider.js.map +1 -0
- package/dist/auth/verifier.js +74 -0
- package/dist/auth/verifier.js.map +1 -0
- package/dist/cli/kill.js +124 -0
- package/dist/cli/kill.js.map +1 -0
- package/dist/cli/main.js +1393 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/menu.js +127 -0
- package/dist/cli/menu.js.map +1 -0
- package/dist/cli/terminal.js +30 -0
- package/dist/cli/terminal.js.map +1 -0
- package/dist/config/globalConfig.js +144 -0
- package/dist/config/globalConfig.js.map +1 -0
- package/dist/config/limits.js +42 -0
- package/dist/config/limits.js.map +1 -0
- package/dist/config/paths.js +94 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/projectConfig.js +63 -0
- package/dist/config/projectConfig.js.map +1 -0
- package/dist/config/stateImport.js +306 -0
- package/dist/config/stateImport.js.map +1 -0
- package/dist/config/tunnelConfig.js +23 -0
- package/dist/config/tunnelConfig.js.map +1 -0
- package/dist/errors.js +82 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluation/contracts.js +86 -0
- package/dist/evaluation/contracts.js.map +1 -0
- package/dist/evaluation/dodoBench.js +69 -0
- package/dist/evaluation/dodoBench.js.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/ipc/authentication.js +117 -0
- package/dist/ipc/authentication.js.map +1 -0
- package/dist/ipc/client.js +86 -0
- package/dist/ipc/client.js.map +1 -0
- package/dist/ipc/installationClient.js +83 -0
- package/dist/ipc/installationClient.js.map +1 -0
- package/dist/ipc/ownerControl.js +46 -0
- package/dist/ipc/ownerControl.js.map +1 -0
- package/dist/ipc/protocol.js +2 -0
- package/dist/ipc/protocol.js.map +1 -0
- package/dist/ipc/server.js +108 -0
- package/dist/ipc/server.js.map +1 -0
- package/dist/platform/execResolve.js +64 -0
- package/dist/platform/execResolve.js.map +1 -0
- package/dist/platform/fileIdentity.js +26 -0
- package/dist/platform/fileIdentity.js.map +1 -0
- package/dist/platform/fsRetry.js +22 -0
- package/dist/platform/fsRetry.js.map +1 -0
- package/dist/platform/pathPolicy.js +29 -0
- package/dist/platform/pathPolicy.js.map +1 -0
- package/dist/platform/privateFs.js +116 -0
- package/dist/platform/privateFs.js.map +1 -0
- package/dist/platform/processTree.js +31 -0
- package/dist/platform/processTree.js.map +1 -0
- package/dist/platform/shell.js +74 -0
- package/dist/platform/shell.js.map +1 -0
- package/dist/platform/speech.js +94 -0
- package/dist/platform/speech.js.map +1 -0
- package/dist/platform/system.js +28 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/platform/windowsSandbox.js +129 -0
- package/dist/platform/windowsSandbox.js.map +1 -0
- package/dist/projects/federation.js +312 -0
- package/dist/projects/federation.js.map +1 -0
- package/dist/projects/registry.js +291 -0
- package/dist/projects/registry.js.map +1 -0
- package/dist/security/accessMode.js +19 -0
- package/dist/security/accessMode.js.map +1 -0
- package/dist/security/approvals.js +56 -0
- package/dist/security/approvals.js.map +1 -0
- package/dist/security/env.js +57 -0
- package/dist/security/env.js.map +1 -0
- package/dist/security/mutationQueue.js +78 -0
- package/dist/security/mutationQueue.js.map +1 -0
- package/dist/security/policy.js +22 -0
- package/dist/security/policy.js.map +1 -0
- package/dist/security/projectAuthority.js +22 -0
- package/dist/security/projectAuthority.js.map +1 -0
- package/dist/security/redact.js +30 -0
- package/dist/security/redact.js.map +1 -0
- package/dist/server/aiAdmin.js +201 -0
- package/dist/server/aiAdmin.js.map +1 -0
- package/dist/server/appServer.js +461 -0
- package/dist/server/appServer.js.map +1 -0
- package/dist/server/bootstrap.js +185 -0
- package/dist/server/bootstrap.js.map +1 -0
- package/dist/server/configUi/app.css +347 -0
- package/dist/server/configUi/app.js +958 -0
- package/dist/server/configUi/index.html +379 -0
- package/dist/server/configUi/ui/alerts.js +92 -0
- package/dist/server/configUi/ui/dom.js +19 -0
- package/dist/server/configUi/ui/tooltips.js +115 -0
- package/dist/server/configUi/vendor/VERSION.txt +1 -0
- package/dist/server/configUi/vendor/sweetalert2.min.css +1 -0
- package/dist/server/configUi/vendor/sweetalert2.min.js +5 -0
- package/dist/server/configUi/workbench.css +62 -0
- package/dist/server/configUi/workbench.js +394 -0
- package/dist/server/installationRuntime.js +155 -0
- package/dist/server/installationRuntime.js.map +1 -0
- package/dist/server/instructions.js +17 -0
- package/dist/server/instructions.js.map +1 -0
- package/dist/server/ipcDispatch.js +225 -0
- package/dist/server/ipcDispatch.js.map +1 -0
- package/dist/server/localConfig.js +561 -0
- package/dist/server/localConfig.js.map +1 -0
- package/dist/server/optionalServices.js +30 -0
- package/dist/server/optionalServices.js.map +1 -0
- package/dist/server/projectLease.js +46 -0
- package/dist/server/projectLease.js.map +1 -0
- package/dist/server/stdioServer.js +75 -0
- package/dist/server/stdioServer.js.map +1 -0
- package/dist/server/version.js +13 -0
- package/dist/server/version.js.map +1 -0
- package/dist/server/workspaceHost.js +194 -0
- package/dist/server/workspaceHost.js.map +1 -0
- package/dist/services/agent/agentService.js +598 -0
- package/dist/services/agent/agentService.js.map +1 -0
- package/dist/services/agent/contracts.js +102 -0
- package/dist/services/agent/contracts.js.map +1 -0
- package/dist/services/ai/adapters.js +287 -0
- package/dist/services/ai/adapters.js.map +1 -0
- package/dist/services/ai/contracts.js +39 -0
- package/dist/services/ai/contracts.js.map +1 -0
- package/dist/services/ai/credentials.js +77 -0
- package/dist/services/ai/credentials.js.map +1 -0
- package/dist/services/ai/network.js +113 -0
- package/dist/services/ai/network.js.map +1 -0
- package/dist/services/ai/settings.js +178 -0
- package/dist/services/ai/settings.js.map +1 -0
- package/dist/services/ai/subagents.js +468 -0
- package/dist/services/ai/subagents.js.map +1 -0
- package/dist/services/assistance/analysis.js +352 -0
- package/dist/services/assistance/analysis.js.map +1 -0
- package/dist/services/assistance/contracts.js +54 -0
- package/dist/services/assistance/contracts.js.map +1 -0
- package/dist/services/assistance/verification.js +274 -0
- package/dist/services/assistance/verification.js.map +1 -0
- package/dist/services/brain/brainParser.js +97 -0
- package/dist/services/brain/brainParser.js.map +1 -0
- package/dist/services/brain/brainService.js +733 -0
- package/dist/services/brain/brainService.js.map +1 -0
- package/dist/services/brain/brainWorker.js +212 -0
- package/dist/services/brain/brainWorker.js.map +1 -0
- package/dist/services/brain/contracts.js +154 -0
- package/dist/services/brain/contracts.js.map +1 -0
- package/dist/services/brain/protocol.js +2 -0
- package/dist/services/brain/protocol.js.map +1 -0
- package/dist/services/changes/applier.js +544 -0
- package/dist/services/changes/applier.js.map +1 -0
- package/dist/services/changes/contentBudget.js +11 -0
- package/dist/services/changes/contentBudget.js.map +1 -0
- package/dist/services/changes/planner.js +284 -0
- package/dist/services/changes/planner.js.map +1 -0
- package/dist/services/changes/types.js +3 -0
- package/dist/services/changes/types.js.map +1 -0
- package/dist/services/context/contextEngine.js +746 -0
- package/dist/services/context/contextEngine.js.map +1 -0
- package/dist/services/context/contracts.js +88 -0
- package/dist/services/context/contracts.js.map +1 -0
- package/dist/services/desktop/desktopPolicy.js +76 -0
- package/dist/services/desktop/desktopPolicy.js.map +1 -0
- package/dist/services/desktop/desktopService.js +130 -0
- package/dist/services/desktop/desktopService.js.map +1 -0
- package/dist/services/desktop/nativeBackend.js +174 -0
- package/dist/services/desktop/nativeBackend.js.map +1 -0
- package/dist/services/desktop/protocol.js +32 -0
- package/dist/services/desktop/protocol.js.map +1 -0
- package/dist/services/files/readService.js +136 -0
- package/dist/services/files/readService.js.map +1 -0
- package/dist/services/git/gitService.js +369 -0
- package/dist/services/git/gitService.js.map +1 -0
- package/dist/services/intelligence/intelService.js +136 -0
- package/dist/services/intelligence/intelService.js.map +1 -0
- package/dist/services/intelligence/protocol.js +2 -0
- package/dist/services/intelligence/protocol.js.map +1 -0
- package/dist/services/intelligence/tsWorker.js +419 -0
- package/dist/services/intelligence/tsWorker.js.map +1 -0
- package/dist/services/jobs/commandInput.js +34 -0
- package/dist/services/jobs/commandInput.js.map +1 -0
- package/dist/services/jobs/jobManager.js +398 -0
- package/dist/services/jobs/jobManager.js.map +1 -0
- package/dist/services/jobs/sandbox.js +156 -0
- package/dist/services/jobs/sandbox.js.map +1 -0
- package/dist/services/jobs/sandboxWiring.js +37 -0
- package/dist/services/jobs/sandboxWiring.js.map +1 -0
- package/dist/services/jobs/spool.js +112 -0
- package/dist/services/jobs/spool.js.map +1 -0
- package/dist/services/jobs/windowsSandboxChild.js +24 -0
- package/dist/services/jobs/windowsSandboxChild.js.map +1 -0
- package/dist/services/jobs/windowsSandboxWorker.js +155 -0
- package/dist/services/jobs/windowsSandboxWorker.js.map +1 -0
- package/dist/services/lsp/documentUri.js +16 -0
- package/dist/services/lsp/documentUri.js.map +1 -0
- package/dist/services/lsp/jsonrpc.js +382 -0
- package/dist/services/lsp/jsonrpc.js.map +1 -0
- package/dist/services/lsp/lspService.js +1051 -0
- package/dist/services/lsp/lspService.js.map +1 -0
- package/dist/services/memory/contracts.js +94 -0
- package/dist/services/memory/contracts.js.map +1 -0
- package/dist/services/memory/memoryService.js +683 -0
- package/dist/services/memory/memoryService.js.map +1 -0
- package/dist/services/multimodal/browserService.js +407 -0
- package/dist/services/multimodal/browserService.js.map +1 -0
- package/dist/services/multimodal/byteRange.js +31 -0
- package/dist/services/multimodal/byteRange.js.map +1 -0
- package/dist/services/multimodal/contracts.js +59 -0
- package/dist/services/multimodal/contracts.js.map +1 -0
- package/dist/services/multimodal/gameService.js +95 -0
- package/dist/services/multimodal/gameService.js.map +1 -0
- package/dist/services/multimodal/images.js +107 -0
- package/dist/services/multimodal/images.js.map +1 -0
- package/dist/services/multimodal/mediaService.js +279 -0
- package/dist/services/multimodal/mediaService.js.map +1 -0
- package/dist/services/multimodal/mediaWorker.js +143 -0
- package/dist/services/multimodal/mediaWorker.js.map +1 -0
- package/dist/services/multimodal/multimodalService.js +51 -0
- package/dist/services/multimodal/multimodalService.js.map +1 -0
- package/dist/services/multimodal/network.js +69 -0
- package/dist/services/multimodal/network.js.map +1 -0
- package/dist/services/multimodal/operations.js +40 -0
- package/dist/services/multimodal/operations.js.map +1 -0
- package/dist/services/multimodal/storage.js +160 -0
- package/dist/services/multimodal/storage.js.map +1 -0
- package/dist/services/multimodal/workflowService.js +179 -0
- package/dist/services/multimodal/workflowService.js.map +1 -0
- package/dist/services/overview.js +284 -0
- package/dist/services/overview.js.map +1 -0
- package/dist/services/resources/casStore.js +304 -0
- package/dist/services/resources/casStore.js.map +1 -0
- package/dist/services/resources/contracts.js +75 -0
- package/dist/services/resources/contracts.js.map +1 -0
- package/dist/services/resources/mime.js +77 -0
- package/dist/services/resources/mime.js.map +1 -0
- package/dist/services/resources/providerRegistry.js +22 -0
- package/dist/services/resources/providerRegistry.js.map +1 -0
- package/dist/services/resources/providers.js +120 -0
- package/dist/services/resources/providers.js.map +1 -0
- package/dist/services/resources/resourceService.js +282 -0
- package/dist/services/resources/resourceService.js.map +1 -0
- package/dist/services/runtime/contracts.js +65 -0
- package/dist/services/runtime/contracts.js.map +1 -0
- package/dist/services/runtime/runtimeService.js +364 -0
- package/dist/services/runtime/runtimeService.js.map +1 -0
- package/dist/services/schedules/scheduleService.js +198 -0
- package/dist/services/schedules/scheduleService.js.map +1 -0
- package/dist/services/search/searchService.js +434 -0
- package/dist/services/search/searchService.js.map +1 -0
- package/dist/services/search/searchWorker.js +58 -0
- package/dist/services/search/searchWorker.js.map +1 -0
- package/dist/setup/componentProbes.js +259 -0
- package/dist/setup/componentProbes.js.map +1 -0
- package/dist/setup/download.js +139 -0
- package/dist/setup/download.js.map +1 -0
- package/dist/setup/managedTools.js +118 -0
- package/dist/setup/managedTools.js.map +1 -0
- package/dist/setup/sandboxProbe.js +172 -0
- package/dist/setup/sandboxProbe.js.map +1 -0
- package/dist/setup/setup.js +522 -0
- package/dist/setup/setup.js.map +1 -0
- package/dist/setup/windowsChromiumAcl.js +56 -0
- package/dist/setup/windowsChromiumAcl.js.map +1 -0
- package/dist/setup/windowsSandboxInstall.js +94 -0
- package/dist/setup/windowsSandboxInstall.js.map +1 -0
- package/dist/setup/windowsSpeechInstall.js +86 -0
- package/dist/setup/windowsSpeechInstall.js.map +1 -0
- package/dist/store/db.js +711 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/scopeEncoding.js +15 -0
- package/dist/store/scopeEncoding.js.map +1 -0
- package/dist/store/store.js +467 -0
- package/dist/store/store.js.map +1 -0
- package/dist/tools/agentRuntimeTools.js +216 -0
- package/dist/tools/agentRuntimeTools.js.map +1 -0
- package/dist/tools/agentTools.js +317 -0
- package/dist/tools/agentTools.js.map +1 -0
- package/dist/tools/assistanceTools.js +126 -0
- package/dist/tools/assistanceTools.js.map +1 -0
- package/dist/tools/brainTools.js +130 -0
- package/dist/tools/brainTools.js.map +1 -0
- package/dist/tools/catalog.js +30 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/tools/changeTools.js +188 -0
- package/dist/tools/changeTools.js.map +1 -0
- package/dist/tools/context.js +243 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/contextTools.js +57 -0
- package/dist/tools/contextTools.js.map +1 -0
- package/dist/tools/coreCatalog.js +82 -0
- package/dist/tools/coreCatalog.js.map +1 -0
- package/dist/tools/desktopTools.js +41 -0
- package/dist/tools/desktopTools.js.map +1 -0
- package/dist/tools/directTools.js +273 -0
- package/dist/tools/directTools.js.map +1 -0
- package/dist/tools/envelope.js +71 -0
- package/dist/tools/envelope.js.map +1 -0
- package/dist/tools/extraTools.js +298 -0
- package/dist/tools/extraTools.js.map +1 -0
- package/dist/tools/gitTools.js +93 -0
- package/dist/tools/gitTools.js.map +1 -0
- package/dist/tools/jobTools.js +454 -0
- package/dist/tools/jobTools.js.map +1 -0
- package/dist/tools/memoryTools.js +102 -0
- package/dist/tools/memoryTools.js.map +1 -0
- package/dist/tools/metaTools.js +153 -0
- package/dist/tools/metaTools.js.map +1 -0
- package/dist/tools/multimodalTools.js +262 -0
- package/dist/tools/multimodalTools.js.map +1 -0
- package/dist/tools/projectTools.js +261 -0
- package/dist/tools/projectTools.js.map +1 -0
- package/dist/tools/resourceTools.js +81 -0
- package/dist/tools/resourceTools.js.map +1 -0
- package/dist/tools/runtimeTools.js +137 -0
- package/dist/tools/runtimeTools.js.map +1 -0
- package/dist/tools/scheduleTools.js +10 -0
- package/dist/tools/scheduleTools.js.map +1 -0
- package/dist/tools/semanticTools.js +141 -0
- package/dist/tools/semanticTools.js.map +1 -0
- package/dist/tools/subagentTools.js +32 -0
- package/dist/tools/subagentTools.js.map +1 -0
- package/dist/tools/surface.js +644 -0
- package/dist/tools/surface.js.map +1 -0
- package/dist/tunnel/control.js +113 -0
- package/dist/tunnel/control.js.map +1 -0
- package/dist/tunnel/credentials.js +392 -0
- package/dist/tunnel/credentials.js.map +1 -0
- package/dist/tunnel/log.js +84 -0
- package/dist/tunnel/log.js.map +1 -0
- package/dist/tunnel/runtime.js +55 -0
- package/dist/tunnel/runtime.js.map +1 -0
- package/dist/tunnel/supervisor.js +280 -0
- package/dist/tunnel/supervisor.js.map +1 -0
- package/dist/util/bytes.js +53 -0
- package/dist/util/bytes.js.map +1 -0
- package/dist/util/hash.js +53 -0
- package/dist/util/hash.js.map +1 -0
- package/dist/workspace/fs.js +305 -0
- package/dist/workspace/fs.js.map +1 -0
- package/dist/workspace/identity.js +17 -0
- package/dist/workspace/identity.js.map +1 -0
- package/dist/workspace/ignores.js +173 -0
- package/dist/workspace/ignores.js.map +1 -0
- package/dist/workspace/root.js +59 -0
- package/dist/workspace/root.js.map +1 -0
- package/docs/AGENT_RUNTIME.md +154 -0
- package/docs/AI_PROVIDERS.md +110 -0
- package/docs/ARCHITECTURE.md +331 -0
- package/docs/ASSISTANCE.md +31 -0
- package/docs/AUTH.md +59 -0
- package/docs/BRAIN.md +92 -0
- package/docs/COMPATIBILITY.md +92 -0
- package/docs/CONFIRMATION.md +13 -0
- package/docs/CONTEXT.md +87 -0
- package/docs/DESKTOP.md +35 -0
- package/docs/EVALUATION.md +86 -0
- package/docs/LARGE_INPUTS.md +21 -0
- package/docs/LSP.md +21 -0
- package/docs/MANUAL_ACCEPTANCE.md +283 -0
- package/docs/MEMORY.md +128 -0
- package/docs/MULTIMODAL.md +32 -0
- package/docs/PROJECTS.md +146 -0
- package/docs/RELEASE_1.0.0.md +111 -0
- package/docs/RELEASE_NOTES.md +92 -0
- package/docs/RESOURCES.md +83 -0
- package/docs/RUNTIME.md +125 -0
- package/docs/SCHEDULES.md +23 -0
- package/docs/SECURITY.md +250 -0
- package/docs/TEST_REPORT.md +312 -0
- package/docs/TUNNEL.md +86 -0
- package/docs/WEB_CLIENTS.md +43 -0
- package/docs/WINDOWS.md +79 -0
- package/docs/adr/001-one-process-one-workspace.md +22 -0
- package/docs/adr/002-external-tunnel.md +19 -0
- package/docs/adr/003-sdk-v2-legacy-stateless.md +20 -0
- package/docs/adr/004-embedded-oauth-provider.md +19 -0
- package/docs/adr/005-static-client-baseline.md +19 -0
- package/docs/adr/006-guarded-fs-explicit-exec.md +16 -0
- package/docs/adr/007-preview-hash-journal.md +18 -0
- package/docs/adr/008-ts-js-semantics-first.md +17 -0
- package/docs/adr/009-jobs-polling.md +18 -0
- package/docs/adr/010-sqlite-better-sqlite3.md +18 -0
- package/docs/adr/011-env-example-secure-default.md +19 -0
- package/docs/adr/012-workspace-scope-grant.md +24 -0
- package/docs/adr/013-loopback-proxy-trust.md +18 -0
- package/docs/adr/014-direct-coding-tier.md +48 -0
- package/docs/adr/015-coding-agent-parity.md +56 -0
- package/docs/adr/016-oidc-openid-consent.md +15 -0
- package/docs/adr/017-installation-identity-local-config.md +44 -0
- package/docs/adr/018-access-scope-encoding.md +11 -0
- package/docs/adr/019-runtime-workspace-switch.md +59 -0
- package/docs/adr/020-desktop-access.md +50 -0
- package/docs/adr/021-persistent-desktop-consent.md +39 -0
- package/docs/adr/022-owner-kill-preserves-login.md +30 -0
- package/docs/adr/023-large-coding-inputs.md +49 -0
- package/docs/adr/024-usage-rounds-and-schedules.md +72 -0
- package/docs/adr/025-owner-chat-whitelist.md +53 -0
- package/docs/adr/026-remove-conversation-confirmation.md +35 -0
- package/docs/adr/027-workspace-client-list.md +20 -0
- package/docs/adr/028-owner-client-deletion.md +25 -0
- package/docs/adr/029-compact-tool-surface.md +121 -0
- package/docs/adr/030-windows-native-candidate.md +71 -0
- package/docs/adr/031-owner-selected-tunnel-supervision.md +13 -0
- package/docs/adr/032-owner-project-registry.md +43 -0
- package/docs/adr/033-read-only-project-federation.md +61 -0
- package/docs/adr/034-universal-resource-cas.md +60 -0
- package/docs/adr/035-project-brain-incremental-index.md +51 -0
- package/docs/adr/036-context-engine-evidence.md +53 -0
- package/docs/adr/037-owner-reviewed-memory.md +46 -0
- package/docs/adr/038-runtime-intelligence.md +46 -0
- package/docs/adr/039-advanced-agent-runtime.md +57 -0
- package/docs/adr/040-dodobench-release-gate.md +43 -0
- package/docs/adr/041-local-macos-docker-linux-gates.md +32 -0
- package/docs/adr/042-project-directory-generation-identity.md +36 -0
- package/docs/adr/043-trusted-self-hosted-platform-gates.md +38 -0
- package/docs/adr/044-global-launcher-and-cli-menu.md +43 -0
- package/docs/adr/045-ai-providers-multiproject.md +56 -0
- package/docs/adr/046-personal-access-mode.md +32 -0
- package/docs/adr/README.md +64 -0
- package/examples/.dodo.json.example +20 -0
- package/examples/cloudflared.yml +23 -0
- package/examples/reverse-proxy-fixture.mjs +37 -0
- package/native/desktop-linux.py +387 -0
- package/native/desktop-windows.cs +180 -0
- package/native/desktop.swift +212 -0
- package/package.json +145 -0
- package/schemas/global-config.schema.json +497 -0
- package/schemas/project-config.schema.json +75 -0
- package/schemas/tools.compact.json +3236 -0
- package/schemas/tools.hybrid.json +8194 -0
- package/schemas/tools.json +34518 -0
- package/scripts/guard-publish.mjs +12 -0
- package/scripts/setup-multimodal.mjs +70 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""DODO Linux X11 backend. JSON stdin; exact executable grants; no clipboard/shell.
|
|
3
|
+
|
|
4
|
+
Uses X-Resource 1.2 for server-reported process identity, XTEST for bounded
|
|
5
|
+
input, Pillow for window-only JPEGs, and the system AT-SPI library for UI text.
|
|
6
|
+
An X11 desktop (including an explicitly chosen nested X11 session) is required.
|
|
7
|
+
Wayland compositor restrictions are never bypassed with privileged input tools.
|
|
8
|
+
"""
|
|
9
|
+
import base64
|
|
10
|
+
import ctypes as C
|
|
11
|
+
import ctypes.util
|
|
12
|
+
import hashlib
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
class Refused(Exception):
|
|
21
|
+
def __init__(self, code, message):
|
|
22
|
+
self.code = code
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
|
|
25
|
+
def require(condition, code, message):
|
|
26
|
+
if not condition:
|
|
27
|
+
raise Refused(code, message)
|
|
28
|
+
|
|
29
|
+
def number(value):
|
|
30
|
+
require(isinstance(value, (int, float)) and not isinstance(value, bool), 'INVALID_INPUT', 'a number is required')
|
|
31
|
+
require(float('-inf') < value < float('inf'), 'INVALID_INPUT', 'invalid numeric value')
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
# X-Resource protocol QueryClientIds, request 4. The server, not _NET_WM_PID,
|
|
35
|
+
# supplies local client PIDs. Works with both older and newer python-xlib.
|
|
36
|
+
def server_pid(display, window_id):
|
|
37
|
+
from Xlib.protocol import rq
|
|
38
|
+
class CountBytes(rq.LengthOf):
|
|
39
|
+
def parse_value(self, length, unused_display):
|
|
40
|
+
return length // 4
|
|
41
|
+
spec = rq.Struct(rq.Card32('client'), rq.Card32('mask'))
|
|
42
|
+
identity = rq.Struct(rq.Object('spec', spec), CountBytes('value', 4), rq.List('value', rq.Card32Obj))
|
|
43
|
+
class Query(rq.ReplyRequest):
|
|
44
|
+
_request = rq.Struct(rq.Card8('opcode'), rq.Opcode(4), rq.RequestLength(), rq.LengthOf('specs', 4), rq.List('specs', spec))
|
|
45
|
+
_reply = rq.Struct(rq.ReplyCode(), rq.Pad(1), rq.Card16('sequence_number'), rq.ReplyLength(), rq.LengthOf('ids', 4), rq.Pad(20), rq.List('ids', identity))
|
|
46
|
+
extension = display.query_extension('X-Resource')
|
|
47
|
+
require(extension and extension.present, 'NOT_SUPPORTED', 'X-Resource 1.2 process identity is required')
|
|
48
|
+
reply = Query(display=display.display, opcode=extension.major_opcode, specs=[{'client': int(window_id), 'mask': 2}])
|
|
49
|
+
for item in reply.ids:
|
|
50
|
+
if item.spec.mask & 2 and item.value:
|
|
51
|
+
return int(item.value[0])
|
|
52
|
+
raise Refused('FORBIDDEN', 'the X server did not provide a local process identity')
|
|
53
|
+
|
|
54
|
+
class Accessibility:
|
|
55
|
+
"""Bounded, read-only AT-SPI traversal; edits are explicit focused-field input."""
|
|
56
|
+
def __init__(self):
|
|
57
|
+
name = ctypes.util.find_library('atspi')
|
|
58
|
+
require(name is not None, 'NOT_SUPPORTED', 'AT-SPI is not installed; run dodo setup')
|
|
59
|
+
self.lib = C.CDLL(name)
|
|
60
|
+
def bind(name, result, arguments):
|
|
61
|
+
function = getattr(self.lib, name)
|
|
62
|
+
function.restype = result
|
|
63
|
+
function.argtypes = arguments
|
|
64
|
+
return function
|
|
65
|
+
p, e = C.c_void_p, C.c_void_p
|
|
66
|
+
self.init = bind('atspi_init', C.c_int, [])
|
|
67
|
+
self.desktop = bind('atspi_get_desktop', p, [C.c_int])
|
|
68
|
+
self.count = bind('atspi_accessible_get_child_count', C.c_int, [p, e])
|
|
69
|
+
self.child = bind('atspi_accessible_get_child_at_index', p, [p, C.c_int, e])
|
|
70
|
+
self.pid = bind('atspi_accessible_get_process_id', C.c_uint, [p, e])
|
|
71
|
+
self.role = bind('atspi_accessible_get_role_name', C.c_char_p, [p, e])
|
|
72
|
+
self.name = bind('atspi_accessible_get_name', C.c_char_p, [p, e])
|
|
73
|
+
self.states = bind('atspi_accessible_get_state_set', p, [p])
|
|
74
|
+
self.contains = bind('atspi_state_set_contains', C.c_int, [p, C.c_int])
|
|
75
|
+
self.text = bind('atspi_accessible_get_text_iface', p, [p])
|
|
76
|
+
self.editable = bind('atspi_accessible_get_editable_text_iface', p, [p])
|
|
77
|
+
self.caret = bind('atspi_text_get_caret_offset', C.c_int, [p, e])
|
|
78
|
+
self.insert = bind('atspi_editable_text_insert_text', C.c_int, [p, C.c_int, C.c_char_p, C.c_int, e])
|
|
79
|
+
self.component = bind('atspi_accessible_get_component_iface', p, [p])
|
|
80
|
+
class Rect(C.Structure):
|
|
81
|
+
_fields_ = [('x', C.c_int), ('y', C.c_int), ('width', C.c_int), ('height', C.c_int)]
|
|
82
|
+
self.extents = bind('atspi_component_get_extents', C.POINTER(Rect), [p, C.c_int, e])
|
|
83
|
+
require(self.init() == 0, 'NOT_SUPPORTED', 'AT-SPI could not connect to the current user session')
|
|
84
|
+
|
|
85
|
+
def elements(self, target, deadline):
|
|
86
|
+
desktop = self.desktop(0)
|
|
87
|
+
require(desktop, 'NOT_SUPPORTED', 'AT-SPI desktop is unavailable')
|
|
88
|
+
queue = []
|
|
89
|
+
for index in range(min(100, max(0, self.count(desktop, None)))):
|
|
90
|
+
app = self.child(desktop, index, None)
|
|
91
|
+
if app and self.pid(app, None) == target['pid']:
|
|
92
|
+
queue.append((app, 0))
|
|
93
|
+
output, scanned = [], 0
|
|
94
|
+
while queue and scanned < 400 and time.time() * 1000 <= deadline:
|
|
95
|
+
element, depth = queue.pop(0)
|
|
96
|
+
scanned += 1
|
|
97
|
+
if not element:
|
|
98
|
+
continue
|
|
99
|
+
role = (self.role(element, None) or b'').decode('utf-8', 'replace')[:100]
|
|
100
|
+
states = self.states(element)
|
|
101
|
+
editable = bool(states and self.contains(states, 7))
|
|
102
|
+
redacted = editable or 'password' in role.lower() or role.lower() in ('text', 'entry')
|
|
103
|
+
component = self.component(element)
|
|
104
|
+
rect = self.extents(component, 0, None) if component else None
|
|
105
|
+
bounds = target['bounds']
|
|
106
|
+
inside = bool(rect and rect.contents.width > 0 and rect.contents.height > 0 and rect.contents.x >= bounds['x'] and rect.contents.y >= bounds['y'] and rect.contents.x + rect.contents.width <= bounds['x'] + bounds['width'] and rect.contents.y + rect.contents.height <= bounds['y'] + bounds['height'])
|
|
107
|
+
if inside:
|
|
108
|
+
output.append((element, states, {'role': role, 'depth': min(depth, 6), 'redacted': redacted, **({} if redacted else {'title': (self.name(element, None) or b'').decode('utf-8', 'replace')[:300]})}))
|
|
109
|
+
if depth < 6 and not redacted:
|
|
110
|
+
for index in range(min(100, max(0, self.count(element, None)))):
|
|
111
|
+
if len(queue) >= 400:
|
|
112
|
+
break
|
|
113
|
+
queue.append((self.child(element, index, None), depth + 1))
|
|
114
|
+
if len(output) >= 100:
|
|
115
|
+
break
|
|
116
|
+
return output, bool(queue)
|
|
117
|
+
|
|
118
|
+
class Backend:
|
|
119
|
+
def __init__(self):
|
|
120
|
+
from Xlib import display, X, XK
|
|
121
|
+
from PIL import Image
|
|
122
|
+
self.X, self.XK, self.Image = X, XK, Image
|
|
123
|
+
display_name = os.environ.get('DISPLAY', '')
|
|
124
|
+
require(re.fullmatch(r':[0-9]+(?:\.[0-9]+)?', display_name), 'NOT_SUPPORTED', 'a local X11 DISPLAY is required; no TCP display or Wayland permission bypass')
|
|
125
|
+
self.display = display.Display(display_name)
|
|
126
|
+
self.root = self.display.screen().root
|
|
127
|
+
self.display.set_error_handler(lambda error, request: None)
|
|
128
|
+
require(self.display.query_extension('X-Resource').present, 'NOT_SUPPORTED', 'X-Resource 1.2 is required for verified window identity')
|
|
129
|
+
|
|
130
|
+
def process(self, pid):
|
|
131
|
+
base = '/proc/' + str(pid)
|
|
132
|
+
require(os.stat(base).st_uid == os.getuid(), 'FORBIDDEN', 'window process belongs to another OS user')
|
|
133
|
+
executable = os.path.realpath(base + '/exe')
|
|
134
|
+
require(os.path.isabs(executable) and os.path.isfile(executable), 'FORBIDDEN', 'window executable is unavailable')
|
|
135
|
+
with open(base + '/stat', encoding='utf-8') as file:
|
|
136
|
+
start = file.read().rsplit(')', 1)[1].split()[19]
|
|
137
|
+
app_id = 'linux.' + hashlib.sha256(executable.encode('utf-8')).hexdigest()[:40]
|
|
138
|
+
return executable, app_id, str(pid) + ':' + start
|
|
139
|
+
|
|
140
|
+
def window(self, window_id, allowed, local=False):
|
|
141
|
+
win = self.display.create_resource_object('window', int(window_id))
|
|
142
|
+
require(win.get_attributes().map_state == self.X.IsViewable, 'FORBIDDEN', 'window is not viewable')
|
|
143
|
+
pid = server_pid(self.display, win.id)
|
|
144
|
+
executable, app_id, instance = self.process(pid)
|
|
145
|
+
require(local or app_id in allowed, 'FORBIDDEN', 'window is outside the exact owner allowlist')
|
|
146
|
+
geometry = win.get_geometry()
|
|
147
|
+
require(0 < geometry.width * geometry.height <= 40000000, 'RESOURCE_LIMIT', 'window dimensions exceed the image budget')
|
|
148
|
+
translated = self.root.translate_coords(win, 0, 0)
|
|
149
|
+
title = win.get_wm_name() or ''
|
|
150
|
+
if isinstance(title, bytes):
|
|
151
|
+
title = title.decode('utf-8', 'replace')
|
|
152
|
+
data = {'windowId': win.id, 'pid': pid, 'appId': app_id, 'processIdentity': instance, 'title': str(title)[:300], 'bounds': {'x': translated.x, 'y': translated.y, 'width': geometry.width, 'height': geometry.height}}
|
|
153
|
+
if local:
|
|
154
|
+
data['executable'] = executable
|
|
155
|
+
return win, data
|
|
156
|
+
|
|
157
|
+
def top(self, win):
|
|
158
|
+
for _ in range(32):
|
|
159
|
+
parent = win.query_tree().parent
|
|
160
|
+
if parent.id == self.root.id:
|
|
161
|
+
return win
|
|
162
|
+
win = parent
|
|
163
|
+
raise Refused('FORBIDDEN', 'window ancestry exceeds the bound')
|
|
164
|
+
|
|
165
|
+
def unobscured(self, win, bounds):
|
|
166
|
+
children = self.root.query_tree().children
|
|
167
|
+
top = self.top(win)
|
|
168
|
+
ids = [item.id for item in children]
|
|
169
|
+
require(top.id in ids, 'CONFLICT', 'window ancestry changed')
|
|
170
|
+
for other in children[ids.index(top.id) + 1:]:
|
|
171
|
+
try:
|
|
172
|
+
if other.get_attributes().map_state != self.X.IsViewable:
|
|
173
|
+
continue
|
|
174
|
+
g = other.get_geometry()
|
|
175
|
+
if g.x < bounds['x'] + bounds['width'] and g.x + g.width > bounds['x'] and g.y < bounds['y'] + bounds['height'] and g.y + g.height > bounds['y']:
|
|
176
|
+
raise Refused('CONFLICT', 'another window covers the target; window-only capture/input refused')
|
|
177
|
+
except Refused:
|
|
178
|
+
raise
|
|
179
|
+
except Exception:
|
|
180
|
+
raise Refused('CONFLICT', 'window stacking changed')
|
|
181
|
+
|
|
182
|
+
def focused(self, win):
|
|
183
|
+
focus = self.display.get_input_focus().focus
|
|
184
|
+
if not hasattr(focus, 'id'):
|
|
185
|
+
return False
|
|
186
|
+
for _ in range(32):
|
|
187
|
+
if focus.id == win.id:
|
|
188
|
+
return True
|
|
189
|
+
if focus.id == self.root.id:
|
|
190
|
+
return False
|
|
191
|
+
focus = focus.query_tree().parent
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
def fresh(self, request, foreground=False):
|
|
195
|
+
require(time.time() * 1000 <= number(request.get('deadline')), 'STALE_WORKSPACE', 'snapshot deadline expired')
|
|
196
|
+
target = request['target']
|
|
197
|
+
win, now = self.window(target['windowId'], request['allowedApps'])
|
|
198
|
+
for key in ('pid', 'appId', 'processIdentity', 'bounds'):
|
|
199
|
+
require(now[key] == target.get(key), 'STALE_WORKSPACE', 'window identity/geometry changed; capture again')
|
|
200
|
+
require(not foreground or self.focused(win), 'CONFLICT', 'target is not focused; input refused')
|
|
201
|
+
return win, now
|
|
202
|
+
|
|
203
|
+
def capture(self, request):
|
|
204
|
+
# Freeze other X clients while checking stacking and obtaining pixels.
|
|
205
|
+
# Closing a failed helper releases the server grab automatically.
|
|
206
|
+
self.display.grab_server()
|
|
207
|
+
try:
|
|
208
|
+
win, data = self.window(request['windowId'], request['allowedApps'])
|
|
209
|
+
b = data['bounds']
|
|
210
|
+
self.unobscured(win, b)
|
|
211
|
+
raw = win.get_image(0, 0, b['width'], b['height'], self.X.ZPixmap, 0xffffffff)
|
|
212
|
+
require(raw is not None, 'NOT_SUPPORTED', 'X server could not capture this window')
|
|
213
|
+
formats = self.display.display.info.pixmap_formats
|
|
214
|
+
pix = next((fmt for fmt in formats if fmt.depth == raw.depth), None)
|
|
215
|
+
require(pix and raw.depth in (24, 32) and pix.bits_per_pixel in (24, 32), 'NOT_SUPPORTED', 'only 24/32-bit TrueColor X11 images are supported')
|
|
216
|
+
require(self.display.display.info.image_byte_order == self.X.LSBFirst, 'NOT_SUPPORTED', 'unsupported X11 image byte order')
|
|
217
|
+
stride = ((b['width'] * pix.bits_per_pixel + pix.scanline_pad - 1) // pix.scanline_pad) * (pix.scanline_pad // 8)
|
|
218
|
+
image = self.Image.frombytes('RGB', (b['width'], b['height']), raw.data, 'raw', 'BGRX' if pix.bits_per_pixel == 32 else 'BGR', stride, 1)
|
|
219
|
+
max_edge = int(number(request.get('maxEdge', 1280)))
|
|
220
|
+
require(1 <= max_edge <= 2000, 'INVALID_INPUT', 'invalid maxEdge')
|
|
221
|
+
image.thumbnail((max_edge, max_edge))
|
|
222
|
+
output = io.BytesIO()
|
|
223
|
+
image.save(output, 'JPEG', quality=75)
|
|
224
|
+
content = output.getvalue()
|
|
225
|
+
require(len(content) <= 3 * 1024 * 1024, 'RESOURCE_LIMIT', 'window JPEG is oversized')
|
|
226
|
+
data.update({'imageWidth': image.width, 'imageHeight': image.height, 'mimeType': 'image/jpeg', 'image': base64.b64encode(content).decode('ascii')})
|
|
227
|
+
return data
|
|
228
|
+
finally:
|
|
229
|
+
self.display.ungrab_server()
|
|
230
|
+
self.display.sync()
|
|
231
|
+
|
|
232
|
+
def position(self, request, x, y):
|
|
233
|
+
win, data = self.fresh(request, True)
|
|
234
|
+
target, bounds = request['target'], data['bounds']
|
|
235
|
+
require(0 <= x < target['imageWidth'] and 0 <= y < target['imageHeight'], 'INVALID_INPUT', 'coordinates outside captured image')
|
|
236
|
+
self.unobscured(win, bounds)
|
|
237
|
+
px = int(bounds['x'] + x * bounds['width'] / target['imageWidth'])
|
|
238
|
+
py = int(bounds['y'] + y * bounds['height'] / target['imageHeight'])
|
|
239
|
+
self.display.xtest_fake_input(self.X.MotionNotify, x=px, y=py)
|
|
240
|
+
self.display.sync()
|
|
241
|
+
|
|
242
|
+
def key(self, request, keysym, modifiers=()):
|
|
243
|
+
code = self.display.keysym_to_keycode(keysym)
|
|
244
|
+
require(code != 0, 'NOT_SUPPORTED', 'key is not present in the current X11 keyboard mapping')
|
|
245
|
+
held = []
|
|
246
|
+
try:
|
|
247
|
+
for symbol in modifiers:
|
|
248
|
+
keycode = self.display.keysym_to_keycode(self.XK.string_to_keysym(symbol))
|
|
249
|
+
require(keycode != 0, 'NOT_SUPPORTED', 'modifier is absent from keyboard mapping')
|
|
250
|
+
held.append(keycode)
|
|
251
|
+
self.display.xtest_fake_input(self.X.KeyPress, keycode)
|
|
252
|
+
self.fresh(request, True)
|
|
253
|
+
held.append(code)
|
|
254
|
+
self.display.xtest_fake_input(self.X.KeyPress, code)
|
|
255
|
+
finally:
|
|
256
|
+
for keycode in reversed(held):
|
|
257
|
+
self.display.xtest_fake_input(self.X.KeyRelease, keycode)
|
|
258
|
+
self.display.sync()
|
|
259
|
+
|
|
260
|
+
def action(self, request):
|
|
261
|
+
require(self.display.query_extension('XTEST').present, 'NOT_SUPPORTED', 'XTEST is not available')
|
|
262
|
+
action = request['action']
|
|
263
|
+
kind = action['kind']
|
|
264
|
+
win, target = self.fresh(request)
|
|
265
|
+
if kind == 'focus':
|
|
266
|
+
win.configure(stack_mode=self.X.Above)
|
|
267
|
+
win.set_input_focus(self.X.RevertToParent, self.X.CurrentTime)
|
|
268
|
+
self.display.sync()
|
|
269
|
+
self.fresh(request, True)
|
|
270
|
+
elif kind == 'type':
|
|
271
|
+
self.fresh(request, True)
|
|
272
|
+
text = action.get('text', '')
|
|
273
|
+
require(isinstance(text, str) and 0 < len(text) <= 2000, 'INVALID_INPUT', 'invalid text length')
|
|
274
|
+
accessibility = Accessibility()
|
|
275
|
+
elements, _ = accessibility.elements(target, request['deadline'])
|
|
276
|
+
candidates = [(element, states) for element, states, _ in elements if states and accessibility.contains(states, 12) and accessibility.editable(element)]
|
|
277
|
+
require(len(candidates) == 1, 'NOT_SUPPORTED', 'typing requires one focused AT-SPI editable field in the permitted window')
|
|
278
|
+
element = candidates[0][0]
|
|
279
|
+
text_interface = accessibility.text(element)
|
|
280
|
+
require(text_interface, 'NOT_SUPPORTED', 'focused control does not expose text insertion')
|
|
281
|
+
offset = accessibility.caret(text_interface, None)
|
|
282
|
+
require(offset >= 0, 'NOT_SUPPORTED', 'focused control has no valid insertion point')
|
|
283
|
+
self.fresh(request, True)
|
|
284
|
+
require(accessibility.insert(accessibility.editable(element), offset, text.encode('utf-8'), len(text), None), 'FORBIDDEN', 'AT-SPI text insertion was rejected')
|
|
285
|
+
elif kind == 'key':
|
|
286
|
+
names = {'enter':'Return','tab':'Tab','space':'space','backspace':'BackSpace','escape':'Escape','delete':'Delete','left':'Left','right':'Right','up':'Up','down':'Down','home':'Home','end':'End','pageup':'Prior','pagedown':'Next'}
|
|
287
|
+
key = action.get('key', '')
|
|
288
|
+
require(re.fullmatch(r'(?:[a-z0-9]|f[1-9]|f1[0-2]|enter|tab|space|backspace|escape|delete|left|right|up|down|home|end|pageup|pagedown)', key), 'INVALID_INPUT', 'unknown key')
|
|
289
|
+
name = names.get(key, key.upper() if key.startswith('f') and len(key) > 1 else key)
|
|
290
|
+
mods = {'command':'Super_L','control':'Control_L','option':'Alt_L','shift':'Shift_L'}
|
|
291
|
+
requested = action.get('modifiers', [])
|
|
292
|
+
require(isinstance(requested, list) and len(requested) <= 4 and all(item in mods for item in requested), 'INVALID_INPUT', 'invalid modifiers')
|
|
293
|
+
self.key(request, self.XK.string_to_keysym(name), [mods[item] for item in requested])
|
|
294
|
+
else:
|
|
295
|
+
require(kind in ('move', 'click', 'scroll', 'drag'), 'INVALID_INPUT', 'unsupported action')
|
|
296
|
+
x, y = number(action.get('x')), number(action.get('y'))
|
|
297
|
+
self.position(request, x, y)
|
|
298
|
+
buttons = []
|
|
299
|
+
if kind == 'click':
|
|
300
|
+
count = int(number(action.get('count', 1)))
|
|
301
|
+
require(count in (1, 2), 'INVALID_INPUT', 'invalid click count')
|
|
302
|
+
buttons = [3 if action.get('button') == 'right' else 1] * count
|
|
303
|
+
elif kind == 'scroll':
|
|
304
|
+
dx, dy = int(number(action.get('deltaX', 0))), int(number(action.get('deltaY', 0)))
|
|
305
|
+
require(abs(dx) <= 1000 and abs(dy) <= 1000, 'INVALID_INPUT', 'scroll limit exceeded')
|
|
306
|
+
buttons = ([5 if dy > 0 else 4] * ((abs(dy) + 119) // 120)) + ([7 if dx > 0 else 6] * ((abs(dx) + 119) // 120))
|
|
307
|
+
elif kind == 'drag':
|
|
308
|
+
tx, ty = number(action.get('toX')), number(action.get('toY'))
|
|
309
|
+
try:
|
|
310
|
+
self.display.xtest_fake_input(self.X.ButtonPress, 1)
|
|
311
|
+
for index in range(1, 11):
|
|
312
|
+
self.position(request, x + (tx - x) * index / 10, y + (ty - y) * index / 10)
|
|
313
|
+
finally:
|
|
314
|
+
self.display.xtest_fake_input(self.X.ButtonRelease, 1)
|
|
315
|
+
self.display.sync()
|
|
316
|
+
for button in buttons:
|
|
317
|
+
self.position(request, x, y)
|
|
318
|
+
try:
|
|
319
|
+
self.display.xtest_fake_input(self.X.ButtonPress, button)
|
|
320
|
+
finally:
|
|
321
|
+
self.display.xtest_fake_input(self.X.ButtonRelease, button)
|
|
322
|
+
self.display.sync()
|
|
323
|
+
return {'posted': True, 'note': 'Input sent to the owner-permitted window. Verify the effect with a fresh observation.'}
|
|
324
|
+
|
|
325
|
+
def run(self, request):
|
|
326
|
+
op = request.get('op')
|
|
327
|
+
if op in ('status', 'requestPermissions'):
|
|
328
|
+
accessibility = False
|
|
329
|
+
if self.display.query_extension('XTEST').present and ctypes.util.find_library('atspi'):
|
|
330
|
+
try:
|
|
331
|
+
bridge = Accessibility()
|
|
332
|
+
accessibility = bool(bridge.desktop(0))
|
|
333
|
+
except Exception:
|
|
334
|
+
accessibility = False
|
|
335
|
+
return {'screenRecording': True, 'accessibility': accessibility, 'platform': 'linux', 'backend': 'x11-xres-xtest-atspi'}
|
|
336
|
+
local = op == 'apps'
|
|
337
|
+
allowed = request.get('allowedApps', [])
|
|
338
|
+
require(local or (isinstance(allowed, list) and 0 < len(allowed) <= 20 and all(isinstance(value, str) for value in allowed)), 'FORBIDDEN', 'exact owner app grants are required')
|
|
339
|
+
if op in ('windows', 'apps'):
|
|
340
|
+
prop = self.root.get_full_property(self.display.intern_atom('_NET_CLIENT_LIST'), self.X.AnyPropertyType)
|
|
341
|
+
ids = list(prop.value) if prop else [win.id for win in self.root.query_tree().children]
|
|
342
|
+
windows = []
|
|
343
|
+
for window_id in ids[:1000]:
|
|
344
|
+
try:
|
|
345
|
+
_, data = self.window(window_id, allowed, local)
|
|
346
|
+
windows.append(data)
|
|
347
|
+
if len(windows) >= 100:
|
|
348
|
+
break
|
|
349
|
+
except Exception:
|
|
350
|
+
continue
|
|
351
|
+
return {'windows': windows, 'truncated': len(ids) > 1000 or len(windows) >= 100}
|
|
352
|
+
if op == 'capture':
|
|
353
|
+
return self.capture(request)
|
|
354
|
+
if op == 'action':
|
|
355
|
+
return self.action(request)
|
|
356
|
+
if op == 'accessibility':
|
|
357
|
+
_, target = self.fresh(request)
|
|
358
|
+
elements, truncated = Accessibility().elements(target, request['deadline'])
|
|
359
|
+
self.fresh(request)
|
|
360
|
+
return {'elements': [row for _, _, row in elements], 'truncated': truncated}
|
|
361
|
+
raise Refused('INVALID_INPUT', 'unknown desktop operation')
|
|
362
|
+
|
|
363
|
+
def main():
|
|
364
|
+
request = {}
|
|
365
|
+
backend = None
|
|
366
|
+
try:
|
|
367
|
+
data = sys.stdin.buffer.read(65537)
|
|
368
|
+
require(len(data) <= 65536, 'RESOURCE_LIMIT', 'desktop request exceeds limit')
|
|
369
|
+
request = json.loads(data.decode('utf-8'))
|
|
370
|
+
require(isinstance(request, dict), 'INVALID_INPUT', 'request must be an object')
|
|
371
|
+
backend = Backend()
|
|
372
|
+
output = {'ok': True, 'data': backend.run(request)}
|
|
373
|
+
except Exception as error:
|
|
374
|
+
code = error.code if isinstance(error, Refused) else 'NOT_SUPPORTED'
|
|
375
|
+
message = str(error) if isinstance(error, Refused) else 'Linux desktop operation failed; verify the local X11 session and setup dependencies'
|
|
376
|
+
if request.get('op') in ('status', 'requestPermissions'):
|
|
377
|
+
output = {'ok': True, 'data': {'screenRecording': False, 'accessibility': False, 'platform': 'linux', 'backend': 'x11-xres-xtest-atspi (session/dependencies unavailable)'}}
|
|
378
|
+
else:
|
|
379
|
+
output = {'ok': False, 'error': {'code': code, 'message': message}}
|
|
380
|
+
finally:
|
|
381
|
+
if backend is not None:
|
|
382
|
+
backend.display.close()
|
|
383
|
+
print(json.dumps(output, ensure_ascii=True, separators=(',', ':')))
|
|
384
|
+
return 0 if output['ok'] else 1
|
|
385
|
+
|
|
386
|
+
if __name__ == '__main__':
|
|
387
|
+
sys.exit(main())
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// DODO owner-permitted window adapter. .NET Framework + inbox Win32 APIs.
|
|
2
|
+
// JSON stdin only; no shell, clipboard, arbitrary code, process launch or privilege elevation.
|
|
3
|
+
using System;
|
|
4
|
+
using System.Collections.Generic;
|
|
5
|
+
using System.Diagnostics;
|
|
6
|
+
using System.Drawing;
|
|
7
|
+
using System.Drawing.Imaging;
|
|
8
|
+
using System.IO;
|
|
9
|
+
using System.Runtime.InteropServices;
|
|
10
|
+
using System.Security.Cryptography;
|
|
11
|
+
using System.Text;
|
|
12
|
+
using System.Threading;
|
|
13
|
+
using System.Web.Script.Serialization;
|
|
14
|
+
using System.Windows.Automation;
|
|
15
|
+
|
|
16
|
+
class DodoDesktop {
|
|
17
|
+
static readonly JavaScriptSerializer Json = new JavaScriptSerializer { MaxJsonLength = 5 * 1024 * 1024, RecursionLimit = 24 };
|
|
18
|
+
delegate bool EnumProc(IntPtr hwnd, IntPtr param);
|
|
19
|
+
[StructLayout(LayoutKind.Sequential)] struct Rect { public int Left, Top, Right, Bottom; }
|
|
20
|
+
[StructLayout(LayoutKind.Sequential)] struct Point { public int X, Y; public Point(int x,int y){X=x;Y=y;} }
|
|
21
|
+
[StructLayout(LayoutKind.Sequential)] struct Mouse { public int X,Y; public uint Data,Flags,Time; public UIntPtr Extra; }
|
|
22
|
+
[StructLayout(LayoutKind.Sequential)] struct Key { public ushort Vk,Scan; public uint Flags,Time; public UIntPtr Extra; }
|
|
23
|
+
[StructLayout(LayoutKind.Explicit)] struct Union { [FieldOffset(0)] public Mouse Mouse; [FieldOffset(0)] public Key Key; }
|
|
24
|
+
[StructLayout(LayoutKind.Sequential)] struct Input { public uint Type; public Union U; }
|
|
25
|
+
[DllImport("user32.dll")] static extern bool EnumWindows(EnumProc proc,IntPtr param);
|
|
26
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hwnd);
|
|
27
|
+
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr hwnd);
|
|
28
|
+
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr hwnd,out Rect rect);
|
|
29
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd,out uint pid);
|
|
30
|
+
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern int GetWindowText(IntPtr hwnd,StringBuilder text,int length);
|
|
31
|
+
[DllImport("user32.dll")] static extern bool PrintWindow(IntPtr hwnd,IntPtr dc,uint flags);
|
|
32
|
+
[DllImport("user32.dll")] static extern IntPtr GetForegroundWindow();
|
|
33
|
+
[DllImport("user32.dll")] static extern bool SetForegroundWindow(IntPtr hwnd);
|
|
34
|
+
[DllImport("user32.dll")] static extern bool SetCursorPos(int x,int y);
|
|
35
|
+
[DllImport("user32.dll")] static extern IntPtr WindowFromPoint(Point point);
|
|
36
|
+
[DllImport("user32.dll")] static extern IntPtr GetAncestor(IntPtr hwnd,uint flags);
|
|
37
|
+
[DllImport("user32.dll",SetLastError=true)] static extern uint SendInput(uint count,Input[] inputs,int size);
|
|
38
|
+
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
|
|
39
|
+
[DllImport("user32.dll")] static extern IntPtr OpenInputDesktop(uint flags,bool inherit,uint access);
|
|
40
|
+
[DllImport("user32.dll")] static extern bool CloseDesktop(IntPtr desktop);
|
|
41
|
+
[DllImport("user32.dll",CharSet=CharSet.Unicode)] static extern bool GetUserObjectInformation(IntPtr obj,int index,StringBuilder value,int length,out int needed);
|
|
42
|
+
[DllImport("kernel32.dll",SetLastError=true)] static extern IntPtr OpenProcess(uint access,bool inherit,uint pid);
|
|
43
|
+
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern bool QueryFullProcessImageName(IntPtr process,uint flags,StringBuilder name,ref int length);
|
|
44
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
45
|
+
static string failureCode="INTERNAL_ERROR";
|
|
46
|
+
static void Refuse(string code,string message){failureCode=code;throw new InvalidOperationException(message);}
|
|
47
|
+
static Dictionary<string,object> Obj(object v){var d=v as Dictionary<string,object>;if(d==null)Refuse("INVALID_INPUT","expected object");return d;}
|
|
48
|
+
static string Str(Dictionary<string,object> d,string k){object v;return d.TryGetValue(k,out v)&&v!=null?Convert.ToString(v):"";}
|
|
49
|
+
static double Num(Dictionary<string,object> d,string k){object v;if(!d.TryGetValue(k,out v))Refuse("INVALID_INPUT","missing number");double n=Convert.ToDouble(v);if(double.IsNaN(n)||double.IsInfinity(n))Refuse("INVALID_INPUT","invalid number");return n;}
|
|
50
|
+
static bool Flag(Dictionary<string,object> d,string k){object v;return d.TryGetValue(k,out v)&&v is bool&&(bool)v;}
|
|
51
|
+
static bool Interactive(){
|
|
52
|
+
if(!Environment.UserInteractive||Process.GetCurrentProcess().SessionId==0)return false;
|
|
53
|
+
var desktop=OpenInputDesktop(0,false,1);if(desktop==IntPtr.Zero)return false;
|
|
54
|
+
try{int needed;var name=new StringBuilder(128);return GetUserObjectInformation(desktop,2,name,256,out needed)&&name.ToString()=="Default";}
|
|
55
|
+
finally{CloseDesktop(desktop);}
|
|
56
|
+
}
|
|
57
|
+
static string ImagePath(uint pid){
|
|
58
|
+
var p=OpenProcess(0x1000,false,pid);if(p==IntPtr.Zero)return null;
|
|
59
|
+
try{var text=new StringBuilder(32768);int length=text.Capacity;if(!QueryFullProcessImageName(p,0,text,ref length))return null;return Path.GetFullPath(text.ToString());}
|
|
60
|
+
finally{CloseHandle(p);}
|
|
61
|
+
}
|
|
62
|
+
static string AppId(string image){using(var h=SHA256.Create()){var bytes=h.ComputeHash(Encoding.UTF8.GetBytes(image.ToUpperInvariant()));return "win."+BitConverter.ToString(bytes).Replace("-","").ToLowerInvariant().Substring(0,40);}}
|
|
63
|
+
static HashSet<string> Allowed(Dictionary<string,object> request){
|
|
64
|
+
object value;if(!request.TryGetValue("allowedApps",out value))Refuse("FORBIDDEN","owner app permission required");
|
|
65
|
+
var array=value as object[];if(array==null||array.Length>20)Refuse("INVALID_INPUT","invalid allowed apps");
|
|
66
|
+
var allowed=new HashSet<string>(StringComparer.Ordinal);foreach(var id in array)allowed.Add(Convert.ToString(id));return allowed;
|
|
67
|
+
}
|
|
68
|
+
static Dictionary<string,object> Window(IntPtr hwnd,HashSet<string> allowed,bool localList){
|
|
69
|
+
if(!Interactive())Refuse("NOT_SUPPORTED","an unlocked interactive Windows desktop is required; Session 0 and secure desktops are refused");
|
|
70
|
+
uint pid;GetWindowThreadProcessId(hwnd,out pid);if(pid==0||!IsWindowVisible(hwnd)||IsIconic(hwnd))return null;
|
|
71
|
+
try{if(Process.GetProcessById((int)pid).SessionId!=Process.GetCurrentProcess().SessionId)return null;}catch{return null;}
|
|
72
|
+
var image=ImagePath(pid);if(image==null)return null;string app=AppId(image);if(!localList&&!allowed.Contains(app))return null;
|
|
73
|
+
Rect r;if(!GetWindowRect(hwnd,out r)||r.Right<=r.Left||r.Bottom<=r.Top||(long)(r.Right-r.Left)*(r.Bottom-r.Top)>40000000)return null;
|
|
74
|
+
var title=new StringBuilder(301);GetWindowText(hwnd,title,title.Capacity);
|
|
75
|
+
var item=new Dictionary<string,object>{{"windowId",hwnd.ToInt64()},{"pid",pid},{"appId",app},{"title",title.ToString()},{"bounds",new Dictionary<string,object>{{"x",r.Left},{"y",r.Top},{"width",r.Right-r.Left},{"height",r.Bottom-r.Top}}}};
|
|
76
|
+
if(localList)item.Add("executable",image);return item;
|
|
77
|
+
}
|
|
78
|
+
static Dictionary<string,object> RequiredWindow(IntPtr hwnd,HashSet<string> allowed){var w=Window(hwnd,allowed,false);if(w==null)Refuse("FORBIDDEN","window is unavailable or not in the owner app allowlist");return w;}
|
|
79
|
+
static void Fresh(Dictionary<string,object> request,Dictionary<string,object> target,HashSet<string> allowed,bool foreground){
|
|
80
|
+
if(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()>Num(request,"deadline"))Refuse("STALE_WORKSPACE","capture a new snapshot; deadline expired");
|
|
81
|
+
var hwnd=new IntPtr(Convert.ToInt64(Num(target,"windowId")));var now=RequiredWindow(hwnd,allowed);
|
|
82
|
+
if(Str(now,"appId")!=Str(target,"appId")||Num(now,"pid")!=Num(target,"pid"))Refuse("STALE_WORKSPACE","window identity changed");
|
|
83
|
+
var a=Obj(now["bounds"]);var b=Obj(target["bounds"]);foreach(var key in new[]{"x","y","width","height"})if(Num(a,key)!=Num(b,key))Refuse("STALE_WORKSPACE","window moved or resized; capture again");
|
|
84
|
+
if(foreground&&GetForegroundWindow()!=hwnd)Refuse("CONFLICT","target window is not foreground; no input was sent");
|
|
85
|
+
}
|
|
86
|
+
static void Send(Input input){if(SendInput(1,new[]{input},Marshal.SizeOf(typeof(Input)))!=1)Refuse("FORBIDDEN","Windows rejected input; no UAC/UIPI bypass was attempted");}
|
|
87
|
+
static Input Keyboard(ushort vk,ushort scan,uint flags){return new Input{Type=1,U=new Union{Key=new Key{Vk=vk,Scan=scan,Flags=flags}}};}
|
|
88
|
+
static Input Pointer(uint flags,int data){return new Input{Type=0,U=new Union{Mouse=new Mouse{Flags=flags,Data=unchecked((uint)data)}}};}
|
|
89
|
+
static Point TargetPoint(Dictionary<string,object> target,double x,double y){
|
|
90
|
+
double width=Num(target,"imageWidth"),height=Num(target,"imageHeight");if(x<0||y<0||x>=width||y>=height)Refuse("INVALID_INPUT","coordinates outside captured image");
|
|
91
|
+
var b=Obj(target["bounds"]);return new Point((int)(Num(b,"x")+x*Num(b,"width")/width),(int)(Num(b,"y")+y*Num(b,"height")/height));
|
|
92
|
+
}
|
|
93
|
+
static void Position(Dictionary<string,object> request,Dictionary<string,object> target,HashSet<string> allowed,double x,double y){
|
|
94
|
+
Fresh(request,target,allowed,true);var p=TargetPoint(target,x,y);var hwnd=new IntPtr(Convert.ToInt64(Num(target,"windowId")));
|
|
95
|
+
if(GetAncestor(WindowFromPoint(p),2)!=hwnd)Refuse("CONFLICT","another window covers the target point; input refused");
|
|
96
|
+
if(!SetCursorPos(p.X,p.Y))Refuse("FORBIDDEN","pointer movement refused");
|
|
97
|
+
}
|
|
98
|
+
static ushort VirtualKey(string key){
|
|
99
|
+
if(key.Length==1&&char.IsLetterOrDigit(key[0]))return (ushort)char.ToUpperInvariant(key[0]);
|
|
100
|
+
int f;if(key.StartsWith("f")&&int.TryParse(key.Substring(1),out f)&&f>=1&&f<=12)return (ushort)(0x70+f-1);
|
|
101
|
+
var map=new Dictionary<string,ushort>{{"enter",13},{"tab",9},{"space",32},{"backspace",8},{"escape",27},{"delete",46},{"left",37},{"up",38},{"right",39},{"down",40},{"home",36},{"end",35},{"pageup",33},{"pagedown",34}};
|
|
102
|
+
ushort v;if(!map.TryGetValue(key,out v))Refuse("INVALID_INPUT","unsupported key");return v;
|
|
103
|
+
}
|
|
104
|
+
static object Action(Dictionary<string,object> request,HashSet<string> allowed){
|
|
105
|
+
var target=Obj(request["target"]);var action=Obj(request["action"]);var hwnd=new IntPtr(Convert.ToInt64(Num(target,"windowId")));string kind=Str(action,"kind");
|
|
106
|
+
Fresh(request,target,allowed,false);
|
|
107
|
+
if(kind=="focus"){
|
|
108
|
+
if(GetForegroundWindow()!=hwnd&&!SetForegroundWindow(hwnd))Refuse("FORBIDDEN","Windows refused focus");Thread.Sleep(75);Fresh(request,target,allowed,true);
|
|
109
|
+
}else{
|
|
110
|
+
Fresh(request,target,allowed,true);
|
|
111
|
+
if(kind=="type"){
|
|
112
|
+
string text=Str(action,"text");if(text.Length==0||text.Length>2000)Refuse("INVALID_INPUT","invalid text length");
|
|
113
|
+
foreach(char c in text){Fresh(request,target,allowed,true);try{Send(Keyboard(0,c,4));}finally{Send(Keyboard(0,c,6));}}
|
|
114
|
+
}else if(kind=="key"){
|
|
115
|
+
ushort key=VirtualKey(Str(action,"key"));var held=new List<ushort>();
|
|
116
|
+
try{object m;if(action.TryGetValue("modifiers",out m)){foreach(var value in (object[])m){var name=Convert.ToString(value);ushort vk=name=="control"?(ushort)17:name=="shift"?(ushort)16:name=="option"?(ushort)18:name=="command"?(ushort)91:(ushort)0;if(vk==0)Refuse("INVALID_INPUT","invalid modifier");Send(Keyboard(vk,0,0));held.Add(vk);}}
|
|
117
|
+
Fresh(request,target,allowed,true);Send(Keyboard(key,0,0));Send(Keyboard(key,0,2));
|
|
118
|
+
}finally{held.Reverse();foreach(var vk in held)Send(Keyboard(vk,0,2));}
|
|
119
|
+
}else{
|
|
120
|
+
double x=Num(action,"x"),y=Num(action,"y");Position(request,target,allowed,x,y);
|
|
121
|
+
if(kind=="click"){
|
|
122
|
+
bool right=Str(action,"button")=="right";int count=action.ContainsKey("count")?(int)Num(action,"count"):1;if(count<1||count>2)Refuse("INVALID_INPUT","invalid click count");
|
|
123
|
+
for(int i=0;i<count;i++){Fresh(request,target,allowed,true);try{Send(Pointer(right?8u:2u,0));}finally{Send(Pointer(right?16u:4u,0));}if(i+1<count)Thread.Sleep(75);}
|
|
124
|
+
}else if(kind=="scroll"){
|
|
125
|
+
int dy=(int)Num(action,"deltaY"),dx=action.ContainsKey("deltaX")?(int)Num(action,"deltaX"):0;if(Math.Abs(dy)>1000||Math.Abs(dx)>1000)Refuse("INVALID_INPUT","scroll too large");
|
|
126
|
+
if(dy!=0)Send(Pointer(0x800,-dy));if(dx!=0)Send(Pointer(0x1000,dx));
|
|
127
|
+
}else if(kind=="drag"){
|
|
128
|
+
double tx=Num(action,"toX"),ty=Num(action,"toY");TargetPoint(target,tx,ty);
|
|
129
|
+
try{Send(Pointer(2,0));for(int i=1;i<=10;i++){Position(request,target,allowed,x+(tx-x)*i/10,y+(ty-y)*i/10);Thread.Sleep(10);}}finally{Send(Pointer(4,0));}
|
|
130
|
+
}else if(kind!="move")Refuse("INVALID_INPUT","unsupported action");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return new{posted=true,note="Input posted to the owner-permitted window. Application effect must be checked in a new observation."};
|
|
134
|
+
}
|
|
135
|
+
static object Capture(Dictionary<string,object> request,HashSet<string> allowed){
|
|
136
|
+
var hwnd=new IntPtr(Convert.ToInt64(Num(request,"windowId")));var window=RequiredWindow(hwnd,allowed);var bounds=Obj(window["bounds"]);
|
|
137
|
+
int width=(int)Num(bounds,"width"),height=(int)Num(bounds,"height"),max=(int)Num(request,"maxEdge");if(max<1||max>2000)Refuse("INVALID_INPUT","invalid image bound");
|
|
138
|
+
using(var raw=new Bitmap(width,height,PixelFormat.Format24bppRgb)){
|
|
139
|
+
using(var g=Graphics.FromImage(raw)){g.Clear(Color.Black);var dc=g.GetHdc();bool ok;try{ok=PrintWindow(hwnd,dc,2);}finally{g.ReleaseHdc(dc);}if(!ok)Refuse("NOT_SUPPORTED","window capture failed (protected or unavailable surface)");}
|
|
140
|
+
var again=RequiredWindow(hwnd,allowed);if(Json.Serialize(again)!=Json.Serialize(window))Refuse("STALE_WORKSPACE","window changed during capture");
|
|
141
|
+
double scale=Math.Min(1.0,(double)max/Math.Max(width,height));int w=Math.Max(1,(int)(width*scale)),h=Math.Max(1,(int)(height*scale));
|
|
142
|
+
using(var image=new Bitmap(raw,w,h))using(var stream=new MemoryStream()){
|
|
143
|
+
image.Save(stream,ImageFormat.Jpeg);if(stream.Length>3*1024*1024)Refuse("RESOURCE_LIMIT","window image too large");
|
|
144
|
+
window.Add("imageWidth",w);window.Add("imageHeight",h);window.Add("mimeType","image/jpeg");window.Add("image",Convert.ToBase64String(stream.ToArray()));
|
|
145
|
+
// OCR is deliberately not fabricated; accessibility is a separate bounded operation.
|
|
146
|
+
return window;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
static object Accessibility(Dictionary<string,object> request,HashSet<string> allowed){
|
|
151
|
+
var target=Obj(request["target"]);Fresh(request,target,allowed,false);var result=new List<object>();bool truncated=false;
|
|
152
|
+
var root=AutomationElement.FromHandle(new IntPtr(Convert.ToInt64(Num(target,"windowId"))));var walker=TreeWalker.ControlViewWalker;
|
|
153
|
+
var pending=new Queue<KeyValuePair<AutomationElement,int>>();pending.Enqueue(new KeyValuePair<AutomationElement,int>(root,0));
|
|
154
|
+
while(pending.Count>0&&result.Count<100){
|
|
155
|
+
Fresh(request,target,allowed,false);var item=pending.Dequeue();var element=item.Key;int depth=item.Value;
|
|
156
|
+
try{var current=element.Current;bool redact=current.IsPassword||current.ControlType==ControlType.Edit;
|
|
157
|
+
var row=new Dictionary<string,object>{{"role",current.ControlType.ProgrammaticName},{"depth",depth},{"redacted",redact}};
|
|
158
|
+
if(!redact){string name=current.Name??"";row.Add("title",name.Substring(0,Math.Min(300,name.Length)));}result.Add(row);
|
|
159
|
+
if(depth<6&&!redact){var child=walker.GetFirstChild(element);int count=0;while(child!=null&&count++<100&&pending.Count<100){pending.Enqueue(new KeyValuePair<AutomationElement,int>(child,depth+1));child=walker.GetNextSibling(child);}if(child!=null)truncated=true;}
|
|
160
|
+
}catch(ElementNotAvailableException){truncated=true;}
|
|
161
|
+
}
|
|
162
|
+
return new{elements=result,truncated=truncated||pending.Count>0};
|
|
163
|
+
}
|
|
164
|
+
static object Run(Dictionary<string,object> request){
|
|
165
|
+
string op=Str(request,"op");if(op=="status"||op=="requestPermissions"){bool ready=Interactive();return new{screenRecording=ready,accessibility=ready,platform="win32",backend="win32-printwindow-uia-sendinput"};}
|
|
166
|
+
bool apps=op=="apps";var allowed=apps?new HashSet<string>():Allowed(request);
|
|
167
|
+
if(op=="windows"||apps){var list=new List<object>();bool truncated=false;
|
|
168
|
+
EnumWindows(delegate(IntPtr hwnd,IntPtr ignored){if(list.Count>=100){truncated=true;return false;}var w=Window(hwnd,allowed,apps);if(w!=null)list.Add(w);return true;},IntPtr.Zero);
|
|
169
|
+
return new{windows=list,truncated=truncated};
|
|
170
|
+
}
|
|
171
|
+
if(op=="capture")return Capture(request,allowed);if(op=="action")return Action(request,allowed);if(op=="accessibility")return Accessibility(request,allowed);
|
|
172
|
+
Refuse("INVALID_INPUT","unknown desktop operation");return null;
|
|
173
|
+
}
|
|
174
|
+
[STAThread] static int Main(){
|
|
175
|
+
Console.InputEncoding=new UTF8Encoding(false);Console.OutputEncoding=new UTF8Encoding(false);SetProcessDPIAware();
|
|
176
|
+
try{var text=new StringBuilder();var buffer=new char[2048];int n;while((n=Console.In.Read(buffer,0,buffer.Length))>0){text.Append(buffer,0,n);if(text.Length>65536)Refuse("RESOURCE_LIMIT","desktop request too large");}
|
|
177
|
+
var data=Run(Obj(Json.DeserializeObject(text.ToString())));Console.Write(Json.Serialize(new{ok=true,data=data}));return 0;
|
|
178
|
+
}catch(Exception e){string message=e is InvalidOperationException?e.Message:"Windows desktop operation failed; no privilege bypass was attempted";Console.Write(Json.Serialize(new{ok=false,error=new{code=failureCode,message=message}}));return 1;}
|
|
179
|
+
}
|
|
180
|
+
}
|