android-midscene-automation 0.2.4 → 1.1.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/CHANGELOG.md +7 -0
- package/README.md +35 -0
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Artemis Applications Package."""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Artemis Admin & Trace Console Package."""
|
|
16
|
+
|
|
17
|
+
from apps.admin_console.server import app
|
|
18
|
+
|
|
19
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Admin Console configuration facade.
|
|
16
|
+
|
|
17
|
+
All configuration and path management is centralized in `artemis.config`.
|
|
18
|
+
This module re-exports common constants for full backward compatibility.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from artemis.config import (
|
|
22
|
+
DB_PATH,
|
|
23
|
+
IMAGES_DIR,
|
|
24
|
+
PAUSE_FILE,
|
|
25
|
+
REPLAY_BASE_DIR,
|
|
26
|
+
TEST_DATA_DIR,
|
|
27
|
+
TEST_OUTPUTS_DIR,
|
|
28
|
+
TRACES_PATH,
|
|
29
|
+
WORKSPACE_ROOT,
|
|
30
|
+
init_ls_address,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"DB_PATH",
|
|
35
|
+
"IMAGES_DIR",
|
|
36
|
+
"PAUSE_FILE",
|
|
37
|
+
"REPLAY_BASE_DIR",
|
|
38
|
+
"TEST_DATA_DIR",
|
|
39
|
+
"TEST_OUTPUTS_DIR",
|
|
40
|
+
"TRACES_PATH",
|
|
41
|
+
"WORKSPACE_ROOT",
|
|
42
|
+
"init_ls_address",
|
|
43
|
+
]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Same-origin security boundary for the Artemis console.
|
|
16
|
+
|
|
17
|
+
Artemis runs without user accounts: whoever can reach the TCP port is the
|
|
18
|
+
operator. For that model to hold for a server that browsers also talk to,
|
|
19
|
+
two browser-borne attack vectors must be closed even on a loopback bind:
|
|
20
|
+
|
|
21
|
+
- DNS rebinding: a page at ``attacker.example`` re-points its DNS record at
|
|
22
|
+
127.0.0.1 so the victim's browser reads API responses under the attacker's
|
|
23
|
+
origin. Blocked by validating the ``Host`` header.
|
|
24
|
+
- Cross-site request forgery: any web page can fire side-effectful requests
|
|
25
|
+
at ``http://127.0.0.1:8000`` from a visitor's browser. Blocked by requiring
|
|
26
|
+
the ``Origin`` header, when a browser sends one, to match the request host.
|
|
27
|
+
|
|
28
|
+
Non-browser clients (the SDK, curl, the CLI, MCP) send no ``Origin`` header
|
|
29
|
+
and pass through untouched. Remote access is expected to arrive over a
|
|
30
|
+
network-level tunnel (Tailscale, SSH port forward); a tunnel hostname that is
|
|
31
|
+
not an IP literal can be admitted via ``ARTEMIS_ALLOWED_HOSTS``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import ipaddress
|
|
37
|
+
import json
|
|
38
|
+
import os
|
|
39
|
+
from urllib.parse import urlsplit
|
|
40
|
+
|
|
41
|
+
_SECURITY_HEADERS: tuple[tuple[bytes, bytes], ...] = (
|
|
42
|
+
(b"x-content-type-options", b"nosniff"),
|
|
43
|
+
(b"referrer-policy", b"no-referrer"),
|
|
44
|
+
(b"x-frame-options", b"DENY"),
|
|
45
|
+
(b"cross-origin-opener-policy", b"same-origin"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Paths whose responses may hold task data, media, or diagnostics and must not
|
|
49
|
+
# land in shared caches or survive on disk after the session.
|
|
50
|
+
_NO_STORE_PREFIXES = ("/api/",)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _hostname(netloc: str) -> str:
|
|
54
|
+
"""Extract the lowercase hostname from a ``host[:port]`` netloc."""
|
|
55
|
+
netloc = netloc.strip().lower()
|
|
56
|
+
if netloc.startswith("["): # IPv6 literal, e.g. [::1]:8000
|
|
57
|
+
return netloc.partition("]")[0].lstrip("[")
|
|
58
|
+
return netloc.rpartition(":")[0] if netloc.count(":") == 1 else netloc
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_ip_literal(host: str) -> bool:
|
|
62
|
+
"""An IP-literal Host cannot be forged through DNS rebinding: a browser
|
|
63
|
+
only sends ``Host: 192.168.1.5`` when it actually connected to that IP."""
|
|
64
|
+
try:
|
|
65
|
+
ipaddress.ip_address(host)
|
|
66
|
+
return True
|
|
67
|
+
except ValueError:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def allowed_hostnames() -> frozenset[str]:
|
|
72
|
+
"""Hostnames (not IP literals) accepted in Host and Origin headers."""
|
|
73
|
+
names = {"localhost"}
|
|
74
|
+
extra = os.environ.get("ARTEMIS_ALLOWED_HOSTS", "")
|
|
75
|
+
names.update(part.strip().lower() for part in extra.split(",") if part.strip())
|
|
76
|
+
return frozenset(names)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def host_is_allowed(host_header: str) -> bool:
|
|
80
|
+
host = _hostname(host_header)
|
|
81
|
+
if not host:
|
|
82
|
+
return False
|
|
83
|
+
return _is_ip_literal(host) or host in allowed_hostnames()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def origin_is_allowed(origin_header: str, host_header: str) -> bool:
|
|
87
|
+
"""A browser request is acceptable when its page is same-origin with this
|
|
88
|
+
server, or was served from an explicitly allowed tunnel hostname."""
|
|
89
|
+
origin = origin_header.strip().lower()
|
|
90
|
+
if not origin or origin == "null":
|
|
91
|
+
return False
|
|
92
|
+
origin_netloc = urlsplit(origin).netloc
|
|
93
|
+
if not origin_netloc:
|
|
94
|
+
return False
|
|
95
|
+
if origin_netloc == host_header.strip().lower():
|
|
96
|
+
return True
|
|
97
|
+
return _hostname(origin_netloc) in allowed_hostnames()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SameOriginBoundaryMiddleware:
|
|
101
|
+
"""Pure ASGI middleware enforcing the Host/Origin boundary and appending
|
|
102
|
+
baseline security headers. Pure ASGI (not BaseHTTPMiddleware) so future
|
|
103
|
+
WebSocket endpoints cannot slip past the Origin check."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, app):
|
|
106
|
+
self.app = app
|
|
107
|
+
|
|
108
|
+
async def __call__(self, scope, receive, send):
|
|
109
|
+
if scope["type"] not in ("http", "websocket"):
|
|
110
|
+
await self.app(scope, receive, send)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if os.environ.get("ARTEMIS_DISABLE_ORIGIN_GUARD", "").lower() in {"1", "true", "yes"}:
|
|
114
|
+
await self._forward(scope, receive, send)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
headers = {k.decode("latin-1").lower(): v.decode("latin-1") for k, v in scope["headers"]}
|
|
118
|
+
host_header = headers.get("host", "")
|
|
119
|
+
origin_header = headers.get("origin")
|
|
120
|
+
|
|
121
|
+
if not host_is_allowed(host_header):
|
|
122
|
+
await self._reject(
|
|
123
|
+
scope,
|
|
124
|
+
send,
|
|
125
|
+
"Unrecognized Host header. Access Artemis via localhost or an IP address, "
|
|
126
|
+
"or add your tunnel hostname to ARTEMIS_ALLOWED_HOSTS.",
|
|
127
|
+
)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
if origin_header is not None and not origin_is_allowed(origin_header, host_header):
|
|
131
|
+
await self._reject(
|
|
132
|
+
scope,
|
|
133
|
+
send,
|
|
134
|
+
"Cross-origin browser requests are not accepted by the Artemis console.",
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
await self._forward(scope, receive, send)
|
|
139
|
+
|
|
140
|
+
async def _forward(self, scope, receive, send):
|
|
141
|
+
if scope["type"] != "http":
|
|
142
|
+
await self.app(scope, receive, send)
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
path = scope.get("path", "")
|
|
146
|
+
|
|
147
|
+
async def send_with_headers(message):
|
|
148
|
+
if message["type"] == "http.response.start":
|
|
149
|
+
raw = list(message.get("headers", []))
|
|
150
|
+
existing = {name.lower() for name, _ in raw}
|
|
151
|
+
for name, value in _SECURITY_HEADERS:
|
|
152
|
+
if name not in existing:
|
|
153
|
+
raw.append((name, value))
|
|
154
|
+
if b"cache-control" not in existing and path.startswith(_NO_STORE_PREFIXES):
|
|
155
|
+
raw.append((b"cache-control", b"no-store"))
|
|
156
|
+
message = {**message, "headers": raw}
|
|
157
|
+
await send(message)
|
|
158
|
+
|
|
159
|
+
await self.app(scope, receive, send_with_headers)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
async def _reject(scope, send, detail: str):
|
|
163
|
+
if scope["type"] == "websocket":
|
|
164
|
+
await send({"type": "websocket.close", "code": 1008})
|
|
165
|
+
return
|
|
166
|
+
body = json.dumps({"detail": detail}).encode("utf-8")
|
|
167
|
+
await send(
|
|
168
|
+
{
|
|
169
|
+
"type": "http.response.start",
|
|
170
|
+
"status": 403,
|
|
171
|
+
"headers": [
|
|
172
|
+
(b"content-type", b"application/json"),
|
|
173
|
+
(b"content-length", str(len(body)).encode("latin-1")),
|
|
174
|
+
*_SECURITY_HEADERS,
|
|
175
|
+
],
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
await send({"type": "http.response.body", "body": body})
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from artemis.config import PAUSE_FILE
|
|
20
|
+
from artemis.runtime.process_probe import pid_is_alive
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ServerState:
|
|
24
|
+
"""Encapsulates all runtime states of the debug server."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.ipc_subscribers: list[Callable[[str, Any], None]] = []
|
|
28
|
+
self.ipc_server: asyncio.Server | None = None
|
|
29
|
+
self.ipc_serve_task: asyncio.Task | None = None
|
|
30
|
+
self.ipc_port: int | None = None
|
|
31
|
+
self.port: int = 8000
|
|
32
|
+
self.host: str = "127.0.0.1"
|
|
33
|
+
self.is_shutting_down: bool = False
|
|
34
|
+
|
|
35
|
+
self.current_process: asyncio.subprocess.Process | None = None
|
|
36
|
+
self.current_goal: str | None = None
|
|
37
|
+
self.current_profile: str | None = None
|
|
38
|
+
self.active_connections: dict[str, dict[str, Any]] = {}
|
|
39
|
+
self.active_session_id: str | None = None
|
|
40
|
+
# Run keys (session id, or the synthetic run key of session-less runs)
|
|
41
|
+
# that were stopped manually. Tracked per run so a manual stop of one
|
|
42
|
+
# device's task never pollutes the terminal-status resolution of a
|
|
43
|
+
# concurrently running task on another device. Entries are discarded by
|
|
44
|
+
# each run's finalizer.
|
|
45
|
+
self.manually_stopped_run_ids: set[str] = set()
|
|
46
|
+
self.cancelled_session_ids: set[str] = set()
|
|
47
|
+
self.startup_progress: dict[str, list[dict[str, Any]]] = {}
|
|
48
|
+
|
|
49
|
+
# Concurrent task executions keyed by session_id. Each value holds
|
|
50
|
+
# {"process", "device_id", "goal", "profile"}. `current_process` /
|
|
51
|
+
# `active_session_id` mirror the most recently launched run for
|
|
52
|
+
# backward compatibility with single-task consumers.
|
|
53
|
+
self.active_runs: dict[str, dict[str, Any]] = {}
|
|
54
|
+
|
|
55
|
+
# Unified single source of truth for task queue
|
|
56
|
+
self.queue_items: list[dict[str, Any]] = []
|
|
57
|
+
self._wake_event: asyncio.Event | None = None
|
|
58
|
+
self._shutdown_event: asyncio.Event | None = None
|
|
59
|
+
self.worker_task: asyncio.Task | None = None
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def wake_event(self) -> asyncio.Event:
|
|
63
|
+
try:
|
|
64
|
+
loop = asyncio.get_running_loop()
|
|
65
|
+
except RuntimeError:
|
|
66
|
+
loop = None
|
|
67
|
+
|
|
68
|
+
if self._wake_event is None or (
|
|
69
|
+
loop
|
|
70
|
+
and getattr(self._wake_event, "_loop", None) is not None
|
|
71
|
+
and self._wake_event._loop != loop
|
|
72
|
+
):
|
|
73
|
+
self._wake_event = asyncio.Event()
|
|
74
|
+
return self._wake_event
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def shutdown_event(self) -> asyncio.Event:
|
|
78
|
+
"""Event set as soon as the HTTP server receives a shutdown signal."""
|
|
79
|
+
try:
|
|
80
|
+
loop = asyncio.get_running_loop()
|
|
81
|
+
except RuntimeError:
|
|
82
|
+
loop = None
|
|
83
|
+
|
|
84
|
+
if self._shutdown_event is None or (
|
|
85
|
+
loop
|
|
86
|
+
and getattr(self._shutdown_event, "_loop", None) is not None
|
|
87
|
+
and self._shutdown_event._loop != loop
|
|
88
|
+
):
|
|
89
|
+
self._shutdown_event = asyncio.Event()
|
|
90
|
+
return self._shutdown_event
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def task_queue(self) -> list[dict[str, Any]]:
|
|
94
|
+
"""Backward compatibility alias for queue_items."""
|
|
95
|
+
return self.queue_items
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def queue_tasks(self) -> list[dict[str, Any]]:
|
|
99
|
+
"""Returns all currently pending tasks in the queue."""
|
|
100
|
+
return [t for t in self.queue_items if isinstance(t, dict) and t.get("status") == "pending"]
|
|
101
|
+
|
|
102
|
+
@queue_tasks.setter
|
|
103
|
+
def queue_tasks(self, val: list[dict[str, Any]]):
|
|
104
|
+
# Compatibility setter
|
|
105
|
+
self.queue_items = list(val)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def queue_goals(self) -> list[str]:
|
|
109
|
+
"""Backward compatibility helper for queue goals."""
|
|
110
|
+
return [t.get("goal", "") for t in self.queue_tasks]
|
|
111
|
+
|
|
112
|
+
@queue_goals.setter
|
|
113
|
+
def queue_goals(self, val: list[Any]):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def prune_finished_runs(self) -> None:
|
|
117
|
+
"""Drop finished entries from active_runs.
|
|
118
|
+
|
|
119
|
+
Besides reaped processes (returncode set), also drops entries whose pid no
|
|
120
|
+
longer exists: if a run coroutine dies without reaping its child, the entry
|
|
121
|
+
must not permanently block the scheduler.
|
|
122
|
+
"""
|
|
123
|
+
for sid, run in list(self.active_runs.items()):
|
|
124
|
+
proc = run.get("process")
|
|
125
|
+
if proc is None or proc.returncode is not None:
|
|
126
|
+
self.active_runs.pop(sid, None)
|
|
127
|
+
continue
|
|
128
|
+
pid = getattr(proc, "pid", None)
|
|
129
|
+
if pid and not pid_is_alive(pid):
|
|
130
|
+
self.active_runs.pop(sid, None)
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def busy_device_ids(self) -> set[str]:
|
|
134
|
+
"""Device serials currently owned by an in-flight run."""
|
|
135
|
+
self.prune_finished_runs()
|
|
136
|
+
return {
|
|
137
|
+
str(run.get("lock_key") or run["device_id"])
|
|
138
|
+
for run in self.active_runs.values()
|
|
139
|
+
if run.get("device_id")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def is_running(self) -> bool:
|
|
144
|
+
self.prune_finished_runs()
|
|
145
|
+
if self.active_runs:
|
|
146
|
+
return True
|
|
147
|
+
has_proc = False
|
|
148
|
+
if self.current_process is not None:
|
|
149
|
+
if self.current_process.returncode is not None:
|
|
150
|
+
has_proc = False
|
|
151
|
+
self.current_process = None
|
|
152
|
+
else:
|
|
153
|
+
pid = getattr(self.current_process, "pid", None)
|
|
154
|
+
if pid and pid_is_alive(pid):
|
|
155
|
+
has_proc = True
|
|
156
|
+
else:
|
|
157
|
+
has_proc = False
|
|
158
|
+
self.current_process = None
|
|
159
|
+
|
|
160
|
+
has_running_item = any(
|
|
161
|
+
isinstance(t, dict) and t.get("status") == "running" for t in self.queue_items
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
has_live_connection = False
|
|
165
|
+
for sid, conn in list(self.active_connections.items()):
|
|
166
|
+
c_pid = conn.get("pid")
|
|
167
|
+
if c_pid:
|
|
168
|
+
if pid_is_alive(c_pid):
|
|
169
|
+
has_live_connection = True
|
|
170
|
+
else:
|
|
171
|
+
self.active_connections.pop(sid, None)
|
|
172
|
+
|
|
173
|
+
if not has_proc and not has_running_item and not has_live_connection:
|
|
174
|
+
self.active_session_id = None
|
|
175
|
+
self.current_process = None
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def is_paused(self) -> bool:
|
|
182
|
+
return PAUSE_FILE.exists()
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def paused_error(self) -> str | None:
|
|
186
|
+
"""Return the persisted pause reason for clients that missed the SSE event."""
|
|
187
|
+
if not PAUSE_FILE.exists():
|
|
188
|
+
return None
|
|
189
|
+
try:
|
|
190
|
+
message = PAUSE_FILE.read_text(encoding="utf-8", errors="replace").strip()
|
|
191
|
+
except OSError:
|
|
192
|
+
return "AI model request failed. The task is paused."
|
|
193
|
+
if message.startswith("LLM Error: "):
|
|
194
|
+
message = message[len("LLM Error: ") :]
|
|
195
|
+
return message or "AI model request failed. The task is paused."
|
|
196
|
+
|
|
197
|
+
def add_subscriber(self, callback: Callable[[str, Any], None]):
|
|
198
|
+
if callback not in self.ipc_subscribers:
|
|
199
|
+
self.ipc_subscribers.append(callback)
|
|
200
|
+
|
|
201
|
+
def remove_subscriber(self, callback: Callable[[str, Any], None]):
|
|
202
|
+
if callback in self.ipc_subscribers:
|
|
203
|
+
self.ipc_subscribers.remove(callback)
|
|
204
|
+
|
|
205
|
+
def record_startup_progress(self, data: dict[str, Any]) -> None:
|
|
206
|
+
"""Retain the short pre-trace timeline so late SSE clients can catch up."""
|
|
207
|
+
session_id = data.get("session_id")
|
|
208
|
+
stage = data.get("stage")
|
|
209
|
+
if not session_id or not stage:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
key = str(session_id)
|
|
213
|
+
events = self.startup_progress.setdefault(key, [])
|
|
214
|
+
replacement_index = next(
|
|
215
|
+
(index for index, item in enumerate(events) if item.get("stage") == stage),
|
|
216
|
+
None,
|
|
217
|
+
)
|
|
218
|
+
snapshot = dict(data)
|
|
219
|
+
if replacement_index is None:
|
|
220
|
+
events.append(snapshot)
|
|
221
|
+
else:
|
|
222
|
+
events[replacement_index] = snapshot
|
|
223
|
+
self.startup_progress[key] = events[-16:]
|
|
224
|
+
|
|
225
|
+
def get_startup_progress(self, session_id: str) -> list[dict[str, Any]]:
|
|
226
|
+
return [dict(item) for item in self.startup_progress.get(str(session_id), [])]
|
|
227
|
+
|
|
228
|
+
def clear_queue(self):
|
|
229
|
+
"""Clears all pending items from the task queue."""
|
|
230
|
+
self.queue_items = [t for t in self.queue_items if t.get("status") == "running"]
|
|
231
|
+
if self._wake_event:
|
|
232
|
+
self._wake_event.set()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# Global shared instance
|
|
236
|
+
state = ServerState()
|
|
237
|
+
|
|
238
|
+
import sys
|
|
239
|
+
|
|
240
|
+
if __name__ == "admin_console.core.state":
|
|
241
|
+
sys.modules["apps.admin_console.core.state"] = sys.modules[__name__]
|
|
242
|
+
elif __name__ == "apps.admin_console.core.state":
|
|
243
|
+
sys.modules["admin_console.core.state"] = sys.modules[__name__]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import sqlite3
|
|
19
|
+
|
|
20
|
+
from artemis.config import DB_PATH
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_initialized_dbs = set()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_db(db_path: Path | None = None) -> sqlite3.Connection:
|
|
28
|
+
"""Returns a SQLite connection with timeout and Row row_factory."""
|
|
29
|
+
path = db_path or DB_PATH
|
|
30
|
+
path_key = str(path)
|
|
31
|
+
if path_key not in _initialized_dbs:
|
|
32
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
try:
|
|
34
|
+
from artemis.data_engine.storage import StorageManager
|
|
35
|
+
|
|
36
|
+
StorageManager(db_path=path, base_trace_dir=path.parent)
|
|
37
|
+
_initialized_dbs.add(path_key)
|
|
38
|
+
except (ImportError, OSError, sqlite3.Error) as exc:
|
|
39
|
+
# Schema bootstrap is best-effort here; queries against a missing
|
|
40
|
+
# schema will surface their own errors, but log the root cause.
|
|
41
|
+
logger.warning("Schema bootstrap failed for %s: %s", path, exc)
|
|
42
|
+
conn = sqlite3.connect(path, timeout=10.0)
|
|
43
|
+
conn.row_factory = sqlite3.Row
|
|
44
|
+
try:
|
|
45
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
46
|
+
conn.execute("PRAGMA busy_timeout=10000")
|
|
47
|
+
except sqlite3.Error:
|
|
48
|
+
# Optional performance PRAGMAs; the connection works without them.
|
|
49
|
+
pass
|
|
50
|
+
return conn
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@contextmanager
|
|
54
|
+
def db_session(db_path: Path | None = None):
|
|
55
|
+
"""Context manager for SQLite connections ensuring clean closure."""
|
|
56
|
+
conn = get_db(db_path)
|
|
57
|
+
try:
|
|
58
|
+
yield conn
|
|
59
|
+
finally:
|
|
60
|
+
conn.close()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|