@mseep/open-computer-use 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/.coderabbit.yaml +25 -0
- package/.dockerignore +95 -0
- package/.env.example +137 -0
- package/.githooks/pre-commit +68 -0
- package/.github/CODEOWNERS +125 -0
- package/.github/ISSUE_TEMPLATE/adr-proposal.md +41 -0
- package/.github/ISSUE_TEMPLATE/bug-report.md +49 -0
- package/.github/ISSUE_TEMPLATE/component-proposal.md +38 -0
- package/.github/ISSUE_TEMPLATE/config.yml +15 -0
- package/.github/ISSUE_TEMPLATE/dependency-proposal.md +59 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/ISSUE_TEMPLATE/nfr-proposal.md +44 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- package/.github/codeql/codeql-config.yml +11 -0
- package/.github/codeql/extensions/security-models/python-sanitizers.model.yml +17 -0
- package/.github/codeql/extensions/security-models/qlpack.yml +7 -0
- package/.github/dependabot.yml +23 -0
- package/.github/security-exceptions.yml +23 -0
- package/.github/workflows/build.yml +420 -0
- package/.github/workflows/codeql.yml +33 -0
- package/.github/workflows/contracts-lint.yml +90 -0
- package/.github/workflows/docs-lint.yml +151 -0
- package/.github/workflows/helm.yml +131 -0
- package/.github/workflows/identity-lint.yml +30 -0
- package/.github/workflows/release-chart.yml +177 -0
- package/.github/workflows/release.yml +95 -0
- package/.github/workflows/security.yml +332 -0
- package/.github/workflows/stale.yml +31 -0
- package/.github/workflows/supply-chain.yml +242 -0
- package/.gitleaks.toml +53 -0
- package/.markdownlint.yaml +51 -0
- package/.semgrepignore +85 -0
- package/.vale/styles/Architecture/ap13-data-class-substrate.yml +12 -0
- package/.vale/styles/Architecture/banned-phrases.yml +23 -0
- package/.vale/styles/Architecture/banned-vocab.yml +23 -0
- package/.vale/styles/Architecture/marketing-tone.yml +19 -0
- package/.vale.ini +18 -0
- package/CHANGELOG.md +411 -0
- package/CLAUDE.md +218 -0
- package/CONTRIBUTING.md +82 -0
- package/Dockerfile +676 -0
- package/LICENSE +98 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/NOTICE +36 -0
- package/README.md +516 -0
- package/SECURITY.md +45 -0
- package/THIRD-PARTY-LICENSES.md +14 -0
- package/apt-packages.txt +108 -0
- package/computer-use-server/.dockerignore +13 -0
- package/computer-use-server/Dockerfile +44 -0
- package/computer-use-server/README.md +84 -0
- package/computer-use-server/app.py +1544 -0
- package/computer-use-server/bin/list-subagent-models +449 -0
- package/computer-use-server/cli-defaults/README.md +31 -0
- package/computer-use-server/cli-defaults/codex.json +7 -0
- package/computer-use-server/cli-defaults/opencode.json +18 -0
- package/computer-use-server/cli_adapters/__init__.py +46 -0
- package/computer-use-server/cli_adapters/claude.py +163 -0
- package/computer-use-server/cli_adapters/codex.py +163 -0
- package/computer-use-server/cli_adapters/opencode.py +169 -0
- package/computer-use-server/cli_adapters/result.py +34 -0
- package/computer-use-server/cli_runtime.py +316 -0
- package/computer-use-server/context_vars.py +24 -0
- package/computer-use-server/docker_manager.py +1100 -0
- package/computer-use-server/docs_html.py +12 -0
- package/computer-use-server/mcp_resources.py +170 -0
- package/computer-use-server/mcp_tools.py +1430 -0
- package/computer-use-server/requirements.txt +17 -0
- package/computer-use-server/security.py +50 -0
- package/computer-use-server/skill_manager.py +664 -0
- package/computer-use-server/static/browser-viewer.js +445 -0
- package/computer-use-server/static/chart.umd.js +14 -0
- package/computer-use-server/static/docs.html +203 -0
- package/computer-use-server/static/github-dark.min.css +10 -0
- package/computer-use-server/static/github.min.css +10 -0
- package/computer-use-server/static/highlight.min.js +1213 -0
- package/computer-use-server/static/highlightjs-line-numbers.min.js +1 -0
- package/computer-use-server/static/icons.js +74 -0
- package/computer-use-server/static/jszip.min.js +13 -0
- package/computer-use-server/static/katex/auto-render.min.js +1 -0
- package/computer-use-server/static/katex/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Bold.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Italic.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Math-Italic.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Script-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/computer-use-server/static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/computer-use-server/static/katex/katex.min.css +1 -0
- package/computer-use-server/static/katex/katex.min.js +1 -0
- package/computer-use-server/static/locale.js +242 -0
- package/computer-use-server/static/mammoth.browser.min.js +21 -0
- package/computer-use-server/static/marked.min.js +6 -0
- package/computer-use-server/static/mermaid.min.js +2811 -0
- package/computer-use-server/static/pdf.min.js +22 -0
- package/computer-use-server/static/pdf.worker.min.js +22 -0
- package/computer-use-server/static/pptxviewjs.min.js +1 -0
- package/computer-use-server/static/preact-htm.min.js +1 -0
- package/computer-use-server/static/preview.css +1030 -0
- package/computer-use-server/static/preview.js +1522 -0
- package/computer-use-server/static/xlsx.full.min.js +22 -0
- package/computer-use-server/static/xterm-addon-fit.min.js +2 -0
- package/computer-use-server/static/xterm-addon-web-links.min.js +2 -0
- package/computer-use-server/static/xterm.css +218 -0
- package/computer-use-server/static/xterm.min.js +2 -0
- package/computer-use-server/system_prompt.py +761 -0
- package/computer-use-server/uploads.py +82 -0
- package/contracts/README.md +53 -0
- package/contracts/audit/audit-fanin.asyncapi.yaml +407 -0
- package/contracts/exec/exec-channel.schema.json +240 -0
- package/contracts/mcp/2025-06-18/ocu-constraints.schema.json +178 -0
- package/contracts/storage/file-artifact-api.schema.json +390 -0
- package/contracts/storage/file-ops.schema.json +217 -0
- package/contracts/storage/mount-config.schema.json +197 -0
- package/cron/Dockerfile +15 -0
- package/cron/cleanup-quick.sh +21 -0
- package/cron/cleanup.sh +127 -0
- package/data/outputs/.gitkeep +0 -0
- package/data/uploads/.gitkeep +0 -0
- package/docker-compose.test.yml +54 -0
- package/docker-compose.webui.yml +77 -0
- package/docker-compose.yml +96 -0
- package/docs/CLOUD.md +29 -0
- package/docs/COMPARISON.md +128 -0
- package/docs/DOCKER.md +469 -0
- package/docs/DYNAMIC-SKILLS.md +77 -0
- package/docs/FEATURES.md +100 -0
- package/docs/INSTALL.md +111 -0
- package/docs/KNOWN-BUGS.md +86 -0
- package/docs/MCP.md +320 -0
- package/docs/SCREENSHOTS.md +39 -0
- package/docs/SKILLS-USER-GUIDE.md +86 -0
- package/docs/SKILLS.md +483 -0
- package/docs/TERMINAL-TAB.md +56 -0
- package/docs/architecture/02-trust-boundaries.md +224 -0
- package/docs/architecture/03-c4-context.md +61 -0
- package/docs/architecture/04-bounded-contexts.md +119 -0
- package/docs/architecture/05-c4-container.md +88 -0
- package/docs/architecture/06-threat-model.md +172 -0
- package/docs/architecture/08-contracts.md +105 -0
- package/docs/architecture/MANIFESTO.md +38 -0
- package/docs/architecture/PROCESS.md +64 -0
- package/docs/architecture/README.md +37 -0
- package/docs/architecture/adr/0000-template.md +65 -0
- package/docs/architecture/adr/0001-layer-0-gate-legacy-exclusion.md +75 -0
- package/docs/architecture/adr/0002-session-view-descriptor.md +57 -0
- package/docs/architecture/adr/0003-sandbox-runtime-tier-ladder.md +63 -0
- package/docs/architecture/adr/0004-operator-authentication-substrate.md +63 -0
- package/docs/architecture/adr/0005-egress-credential-delivery-envoy-sds.md +62 -0
- package/docs/architecture/adr/0006-egress-forward-proxy-substrate.md +65 -0
- package/docs/architecture/adr/0007-egress-auth-mechanism.md +72 -0
- package/docs/architecture/adr/0008-session-egress-attribution.md +59 -0
- package/docs/architecture/adr/0009-audit-pipeline-pluggable-by-contract.md +76 -0
- package/docs/architecture/adr/0010-storage-backend-pluggable-adapter.md +60 -0
- package/docs/architecture/adr/0011-storage-egress-lane.md +67 -0
- package/docs/architecture/adr/0012-implementation-language.md +67 -0
- package/docs/architecture/adr/0020-sandbox-image-provisioning.md +82 -0
- package/docs/architecture/adr/README.md +53 -0
- package/docs/architecture/compliance/.gitkeep +0 -0
- package/docs/architecture/components/00-overview.md +42 -0
- package/docs/architecture/components/0000-template.md +50 -0
- package/docs/architecture/components/01-mcp-gateway.md +80 -0
- package/docs/architecture/components/02-control-operator-api.md +80 -0
- package/docs/architecture/components/04-storage-broker.md +104 -0
- package/docs/architecture/components/05-session-sandbox.md +93 -0
- package/docs/architecture/components/06-egress-trust-edge.md +95 -0
- package/docs/architecture/components/07-audit-pipeline.md +110 -0
- package/docs/architecture/diagrams/.gitkeep +0 -0
- package/docs/architecture/diagrams/02-trust-boundaries.mmd +111 -0
- package/docs/architecture/diagrams/06-threat-model.mmd +41 -0
- package/docs/architecture/diagrams/08-contracts.mmd +47 -0
- package/docs/architecture/diagrams/c4-container.mmd +59 -0
- package/docs/architecture/diagrams/c4-context.mmd +46 -0
- package/docs/architecture/glossary.md +172 -0
- package/docs/architecture/manifesto/.gitkeep +0 -0
- package/docs/architecture/manifesto/01-audience-and-buyer.md +57 -0
- package/docs/architecture/manifesto/02-nfrs.md +325 -0
- package/docs/architecture/manifesto/03-non-negotiables.md +35 -0
- package/docs/architecture/manifesto/04-non-goals.md +23 -0
- package/docs/architecture/manifesto/05-licensing-posture.md +61 -0
- package/docs/architecture/manifesto/06-starter-mode-policy.md +49 -0
- package/docs/architecture/manifesto/07-governance.md +60 -0
- package/docs/architecture/primitives-backlog.md +51 -0
- package/docs/architecture.svg +117 -0
- package/docs/claude-code-gateway.md +173 -0
- package/docs/cli-config-templates.md +240 -0
- package/docs/data-flow.svg +72 -0
- package/docs/demo-landing-page.gif +0 -0
- package/docs/demo-qwen-trending.gif +0 -0
- package/docs/dynamic-skills.svg +77 -0
- package/docs/file-flow.svg +126 -0
- package/docs/future-architecture/README.md +152 -0
- package/docs/future-architecture/adr/0001-control-plane-language-go.md +80 -0
- package/docs/future-architecture/adr/0002-guest-agent-language-go.md +84 -0
- package/docs/future-architecture/adr/0003-docker-poc-first-then-k8s.md +37 -0
- package/docs/future-architecture/adr/0004-pluggable-runtime-via-runtimeclass.md +34 -0
- package/docs/future-architecture/adr/0005-mcp-as-control-plane-gateway.md +34 -0
- package/docs/future-architecture/adr/0006-no-agpl-no-bsl-dependencies.md +41 -0
- package/docs/future-architecture/adr/0007-superseded-by-future-architecture.md +37 -0
- package/docs/future-architecture/adr/0008-internal-grpc-external-rest-mcp.md +106 -0
- package/docs/future-architecture/adr/0009-external-protocol-dialects.md +94 -0
- package/docs/future-architecture/adr/0010-lambda-as-inspiration-not-runtime.md +86 -0
- package/docs/future-architecture/adr/0011-kata-as-first-class-dind-runtime.md +84 -0
- package/docs/future-architecture/antipatterns.md +552 -0
- package/docs/future-architecture/architecture/01-layers.md +109 -0
- package/docs/future-architecture/architecture/02-layer4-control-plane.md +122 -0
- package/docs/future-architecture/architecture/03-layer3-providers.md +174 -0
- package/docs/future-architecture/architecture/04-layer2-runtimes.md +114 -0
- package/docs/future-architecture/architecture/04b-credential-broker.md +153 -0
- package/docs/future-architecture/architecture/05-layer1-guest-agent.md +138 -0
- package/docs/future-architecture/architecture/06-storage.md +134 -0
- package/docs/future-architecture/architecture/07-security.md +194 -0
- package/docs/future-architecture/architecture/08-networking.md +149 -0
- package/docs/future-architecture/architecture/09-templates.md +122 -0
- package/docs/future-architecture/architecture/10-observability.md +121 -0
- package/docs/future-architecture/design-notes.md +72 -0
- package/docs/future-architecture/gaps.md +281 -0
- package/docs/future-architecture/phase-template.md +123 -0
- package/docs/future-architecture/references.md +225 -0
- package/docs/future-architecture/research/01-kata-containers.md +100 -0
- package/docs/future-architecture/research/02-e2b-infra.md +133 -0
- package/docs/future-architecture/research/03-coder.md +115 -0
- package/docs/future-architecture/research/04-cloud-hypervisor.md +99 -0
- package/docs/future-architecture/research/05-firecracker.md +114 -0
- package/docs/future-architecture/research/06-agent-sandbox.md +142 -0
- package/docs/future-architecture/research/07-chromedp.md +78 -0
- package/docs/future-architecture/research/08-microsandbox.md +78 -0
- package/docs/future-architecture/research/09-agentbox.md +135 -0
- package/docs/future-architecture/research/10-sysbox.md +100 -0
- package/docs/future-architecture/research/11-firecracker-containerd.md +93 -0
- package/docs/future-architecture/research/12-docker-socket-proxy.md +59 -0
- package/docs/future-architecture/research/14-e2b-desktop-and-surf.md +107 -0
- package/docs/future-architecture/research/18-open-webui-terminals-observed.md +135 -0
- package/docs/future-architecture/research/bank-buyer.md +96 -0
- package/docs/future-architecture/research/enthusiast-audience.md +106 -0
- package/docs/future-architecture/research/proof-uipath-anthropic-2026-05.md +76 -0
- package/docs/future-architecture/research/widemoat-thesis-advisor.md +124 -0
- package/docs/future-architecture/roadmap.md +438 -0
- package/docs/kata-runtime.md +267 -0
- package/docs/kubernetes.md +86 -0
- package/docs/logo.png +0 -0
- package/docs/multi-cli.md +161 -0
- package/docs/openwebui-filter.md +134 -0
- package/docs/roadmap/implementation-roadmap.md +104 -0
- package/docs/sandbox-contents.svg +229 -0
- package/docs/screenshots/01-create-document.png +0 -0
- package/docs/screenshots/02-file-preview.png +0 -0
- package/docs/screenshots/03-browser-viewer.png +0 -0
- package/docs/screenshots/04-sub-agent-terminal.png +0 -0
- package/docs/screenshots/05-chat-overview.png +0 -0
- package/docs/screenshots/06-sub-agent-dashboard.png +0 -0
- package/docs/screenshots/07-frontend-design-skill.png +0 -0
- package/docs/screenshots/08-pptx-skill.png +0 -0
- package/docs/screenshots/09-skill-creator.png +0 -0
- package/docs/screenshots/10-data-chart.png +0 -0
- package/docs/shared-browser.svg +102 -0
- package/docs/system-prompt.md +113 -0
- package/docs/terminal-flow.svg +69 -0
- package/examples/helm/README.md +20 -0
- package/examples/helm/standalone/values.yaml +49 -0
- package/examples/helm/with-open-webui/README.md +99 -0
- package/examples/helm/with-open-webui/values-computer-use.yaml +32 -0
- package/examples/helm/with-open-webui/values-open-webui.yaml +67 -0
- package/fonts/NotoEmoji-Regular.ttf +0 -0
- package/helm/computer-use-server/.helmignore +17 -0
- package/helm/computer-use-server/Chart.yaml +32 -0
- package/helm/computer-use-server/README.md +211 -0
- package/helm/computer-use-server/templates/NOTES.txt +66 -0
- package/helm/computer-use-server/templates/_helpers.tpl +115 -0
- package/helm/computer-use-server/templates/configmap-dind-init.yaml +82 -0
- package/helm/computer-use-server/templates/configmap.yaml +18 -0
- package/helm/computer-use-server/templates/deployment.yaml +248 -0
- package/helm/computer-use-server/templates/ingress.yaml +38 -0
- package/helm/computer-use-server/templates/networkpolicy.yaml +50 -0
- package/helm/computer-use-server/templates/pdb.yaml +16 -0
- package/helm/computer-use-server/templates/pvc-data.yaml +20 -0
- package/helm/computer-use-server/templates/pvc-skills-cache.yaml +20 -0
- package/helm/computer-use-server/templates/pvc-user-data.yaml +20 -0
- package/helm/computer-use-server/templates/pvc-var-lib-docker.yaml +27 -0
- package/helm/computer-use-server/templates/secret.yaml +23 -0
- package/helm/computer-use-server/templates/service.yaml +22 -0
- package/helm/computer-use-server/templates/serviceaccount.yaml +15 -0
- package/helm/computer-use-server/templates/tests/test-health.yaml +23 -0
- package/helm/computer-use-server/values.schema.json +183 -0
- package/helm/computer-use-server/values.yaml +297 -0
- package/lychee.toml +36 -0
- package/openwebui/Dockerfile +52 -0
- package/openwebui/README.md +38 -0
- package/openwebui/functions/README.md +48 -0
- package/openwebui/functions/computer_link_filter.py +487 -0
- package/openwebui/init.sh +305 -0
- package/openwebui/patches/README.md +44 -0
- package/openwebui/patches/fix_artifacts_auto_show.py +441 -0
- package/openwebui/patches/fix_attached_files_position.py +87 -0
- package/openwebui/patches/fix_large_tool_args.py +156 -0
- package/openwebui/patches/fix_large_tool_results.py +289 -0
- package/openwebui/patches/fix_preview_url_detection.py +230 -0
- package/openwebui/patches/fix_skip_embedding_chat_files.py +229 -0
- package/openwebui/patches/fix_skip_rag_files_native_fc.py +100 -0
- package/openwebui/patches/fix_tool_loop_errors.py +510 -0
- package/package.json +39 -0
- package/requirements.txt +112 -0
- package/scripts/check-config.sh +141 -0
- package/scripts/docs-lint/ai-slop-detector.sh +202 -0
- package/scripts/docs-lint/architecture-tree-whitelist.sh +131 -0
- package/scripts/docs-lint/ascii-diagram-detector.sh +58 -0
- package/scripts/docs-lint/front-matter-validator.sh +97 -0
- package/scripts/docs-lint/gitignored-ref-detector.sh +122 -0
- package/scripts/docs-lint/identity-email-detector.sh +48 -0
- package/scripts/docs-lint/test-linters.sh +354 -0
- package/scripts/docs-lint/wc-budget.sh +61 -0
- package/scripts/githooks/pre-push +75 -0
- package/server.json +13 -0
- package/settings-wrapper/Dockerfile +9 -0
- package/settings-wrapper/README.md +119 -0
- package/settings-wrapper/app.py +113 -0
- package/settings-wrapper/requirements.txt +2 -0
- package/settings-wrapper/skills.json +25 -0
- package/skills/README.md +46 -0
- package/skills/examples/algorithmic-art/SKILL.md +405 -0
- package/skills/examples/algorithmic-art/templates/generator_template.js +223 -0
- package/skills/examples/algorithmic-art/templates/viewer.html +601 -0
- package/skills/examples/artifacts-builder/SKILL.md +74 -0
- package/skills/examples/artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/examples/artifacts-builder/scripts/init-artifact.sh +322 -0
- package/skills/examples/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/examples/canvas-design/LICENSE.txt +202 -0
- package/skills/examples/canvas-design/SKILL.md +130 -0
- package/skills/examples/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/BigShoulders-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Boldonse-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/DMMono-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/EricaOne-OFL.txt +94 -0
- package/skills/examples/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/GeistMono-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Gloock-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Italiana-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Jura-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/NationalPark-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/PixelifySans-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/PoiretOne-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/RedHatMono-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Silkscreen-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/SmoochSans-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/WorkSans-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
- package/skills/examples/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
- package/skills/examples/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
- package/skills/examples/copy-editing/SKILL.md +447 -0
- package/skills/examples/copy-editing/evals/evals.json +89 -0
- package/skills/examples/copy-editing/references/plain-english-alternatives.md +394 -0
- package/skills/examples/internal-comms/LICENSE.txt +202 -0
- package/skills/examples/internal-comms/SKILL.md +32 -0
- package/skills/examples/internal-comms/examples/3p-updates.md +47 -0
- package/skills/examples/internal-comms/examples/company-newsletter.md +65 -0
- package/skills/examples/internal-comms/examples/faq-answers.md +30 -0
- package/skills/examples/internal-comms/examples/general-comms.md +16 -0
- package/skills/examples/mcp-builder/SKILL.md +328 -0
- package/skills/examples/mcp-builder/reference/evaluation.md +602 -0
- package/skills/examples/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/skills/examples/mcp-builder/reference/node_mcp_server.md +916 -0
- package/skills/examples/mcp-builder/reference/python_mcp_server.md +752 -0
- package/skills/examples/mcp-builder/scripts/connections.py +151 -0
- package/skills/examples/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/examples/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/examples/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/examples/product-marketing-context/SKILL.md +241 -0
- package/skills/examples/product-marketing-context/evals/evals.json +85 -0
- package/skills/examples/single-cell-rna-qc/SKILL.md +175 -0
- package/skills/examples/single-cell-rna-qc/references/scverse_qc_guidelines.md +186 -0
- package/skills/examples/single-cell-rna-qc/scripts/qc_analysis.py +232 -0
- package/skills/examples/single-cell-rna-qc/scripts/qc_core.py +233 -0
- package/skills/examples/single-cell-rna-qc/scripts/qc_plotting.py +235 -0
- package/skills/examples/skill-creator/SKILL.md +355 -0
- package/skills/examples/skill-creator/references/output-patterns.md +82 -0
- package/skills/examples/skill-creator/references/workflows.md +28 -0
- package/skills/examples/skill-creator/scripts/init_skill.py +303 -0
- package/skills/examples/skill-creator/scripts/package_skill.py +110 -0
- package/skills/examples/skill-creator/scripts/quick_validate.py +95 -0
- package/skills/examples/slack-gif-creator/SKILL.md +254 -0
- package/skills/examples/slack-gif-creator/core/easing.py +234 -0
- package/skills/examples/slack-gif-creator/core/frame_composer.py +176 -0
- package/skills/examples/slack-gif-creator/core/gif_builder.py +269 -0
- package/skills/examples/slack-gif-creator/core/validators.py +136 -0
- package/skills/examples/slack-gif-creator/requirements.txt +4 -0
- package/skills/examples/social-content/SKILL.md +278 -0
- package/skills/examples/social-content/evals/evals.json +92 -0
- package/skills/examples/social-content/references/platforms.md +170 -0
- package/skills/examples/social-content/references/post-templates.md +177 -0
- package/skills/examples/social-content/references/reverse-engineering.md +195 -0
- package/skills/examples/theme-factory/SKILL.md +59 -0
- package/skills/examples/theme-factory/theme-showcase.pdf +0 -0
- package/skills/examples/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/examples/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/examples/theme-factory/themes/desert-rose.md +19 -0
- package/skills/examples/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/examples/theme-factory/themes/golden-hour.md +19 -0
- package/skills/examples/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/examples/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/examples/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/examples/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/examples/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/examples/web-artifacts-builder/LICENSE.txt +202 -0
- package/skills/examples/web-artifacts-builder/SKILL.md +74 -0
- package/skills/examples/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/examples/web-artifacts-builder/scripts/init-artifact.sh +322 -0
- package/skills/examples/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/examples/writing-skills/SKILL.md +655 -0
- package/skills/examples/writing-skills/anthropic-best-practices.md +1150 -0
- package/skills/examples/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/skills/examples/writing-skills/graphviz-conventions.dot +172 -0
- package/skills/examples/writing-skills/persuasion-principles.md +187 -0
- package/skills/examples/writing-skills/render-graphs.js +168 -0
- package/skills/examples/writing-skills/testing-skills-with-subagents.md +384 -0
- package/skills/public/describe-image/SKILL.md +105 -0
- package/skills/public/describe-image/scripts/describe.py +389 -0
- package/skills/public/doc-coauthoring/SKILL.md +375 -0
- package/skills/public/docx/LICENSE.txt +30 -0
- package/skills/public/docx/SKILL.md +199 -0
- package/skills/public/docx/docx-js.md +350 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/public/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/public/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/public/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/public/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/public/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/public/docx/ooxml/schemas/mce/mc.xsd +75 -0
- package/skills/public/docx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/public/docx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/public/docx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/public/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/public/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/public/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/public/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/public/docx/ooxml/scripts/pack.py +159 -0
- package/skills/public/docx/ooxml/scripts/unpack.py +29 -0
- package/skills/public/docx/ooxml/scripts/validate.py +69 -0
- package/skills/public/docx/ooxml/scripts/validation/__init__.py +15 -0
- package/skills/public/docx/ooxml/scripts/validation/base.py +951 -0
- package/skills/public/docx/ooxml/scripts/validation/docx.py +274 -0
- package/skills/public/docx/ooxml/scripts/validation/pptx.py +315 -0
- package/skills/public/docx/ooxml/scripts/validation/redlining.py +279 -0
- package/skills/public/docx/ooxml.md +632 -0
- package/skills/public/docx/scripts/__init__.py +1 -0
- package/skills/public/docx/scripts/document.py +1292 -0
- package/skills/public/docx/scripts/templates/comments.xml +3 -0
- package/skills/public/docx/scripts/templates/commentsExtended.xml +3 -0
- package/skills/public/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/public/docx/scripts/templates/commentsIds.xml +3 -0
- package/skills/public/docx/scripts/templates/people.xml +3 -0
- package/skills/public/docx/scripts/utilities.py +374 -0
- package/skills/public/file-reading/LICENSE.txt +30 -0
- package/skills/public/file-reading/SKILL.md +350 -0
- package/skills/public/frontend-design/LICENSE.txt +177 -0
- package/skills/public/frontend-design/SKILL.md +42 -0
- package/skills/public/gitlab-explorer/SKILL.md +174 -0
- package/skills/public/gitlab-explorer/references/git-commands.md +323 -0
- package/skills/public/gitlab-explorer/references/glab-commands.md +282 -0
- package/skills/public/gitlab-explorer/scripts/check_gitlab_auth.sh +109 -0
- package/skills/public/pdf/FORMS.md +205 -0
- package/skills/public/pdf/REFERENCE.md +612 -0
- package/skills/public/pdf/SKILL.md +364 -0
- package/skills/public/pdf/scripts/check_bounding_boxes.py +70 -0
- package/skills/public/pdf/scripts/check_bounding_boxes_test.py +226 -0
- package/skills/public/pdf/scripts/check_fillable_fields.py +12 -0
- package/skills/public/pdf/scripts/convert_pdf_to_images.py +35 -0
- package/skills/public/pdf/scripts/create_validation_image.py +41 -0
- package/skills/public/pdf/scripts/extract_form_field_info.py +152 -0
- package/skills/public/pdf/scripts/fill_fillable_fields.py +114 -0
- package/skills/public/pdf/scripts/fill_pdf_form_with_annotations.py +108 -0
- package/skills/public/pdf-reading/LICENSE.txt +30 -0
- package/skills/public/pdf-reading/REFERENCE.md +196 -0
- package/skills/public/pdf-reading/SKILL.md +305 -0
- package/skills/public/playwright-cli/SKILL.md +278 -0
- package/skills/public/playwright-cli/references/request-mocking.md +87 -0
- package/skills/public/playwright-cli/references/running-code.md +232 -0
- package/skills/public/playwright-cli/references/session-management.md +169 -0
- package/skills/public/playwright-cli/references/storage-state.md +275 -0
- package/skills/public/playwright-cli/references/test-generation.md +88 -0
- package/skills/public/playwright-cli/references/tracing.md +139 -0
- package/skills/public/playwright-cli/references/video-recording.md +43 -0
- package/skills/public/pptx/LICENSE.txt +30 -0
- package/skills/public/pptx/SKILL.md +484 -0
- package/skills/public/pptx/css.md +335 -0
- package/skills/public/pptx/html2pptx.md +893 -0
- package/skills/public/pptx/html2pptx.tgz +0 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/public/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/public/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/public/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/public/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/public/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/public/pptx/ooxml/schemas/mce/mc.xsd +75 -0
- package/skills/public/pptx/ooxml/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/public/pptx/ooxml/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/public/pptx/ooxml/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/public/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/public/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/public/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/public/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/public/pptx/ooxml/scripts/pack.py +159 -0
- package/skills/public/pptx/ooxml/scripts/unpack.py +29 -0
- package/skills/public/pptx/ooxml/scripts/validate.py +69 -0
- package/skills/public/pptx/ooxml/scripts/validation/__init__.py +15 -0
- package/skills/public/pptx/ooxml/scripts/validation/base.py +951 -0
- package/skills/public/pptx/ooxml/scripts/validation/docx.py +274 -0
- package/skills/public/pptx/ooxml/scripts/validation/pptx.py +315 -0
- package/skills/public/pptx/ooxml/scripts/validation/redlining.py +279 -0
- package/skills/public/pptx/ooxml.md +427 -0
- package/skills/public/pptx/scripts/inventory.py +1020 -0
- package/skills/public/pptx/scripts/rearrange.py +231 -0
- package/skills/public/pptx/scripts/replace.py +385 -0
- package/skills/public/pptx/scripts/thumbnail.py +450 -0
- package/skills/public/skill-creator/SKILL.md +356 -0
- package/skills/public/skill-creator/references/output-patterns.md +82 -0
- package/skills/public/skill-creator/references/workflows.md +28 -0
- package/skills/public/skill-creator/scripts/init_skill.py +303 -0
- package/skills/public/skill-creator/scripts/package_skill.py +110 -0
- package/skills/public/skill-creator/scripts/quick_validate.py +95 -0
- package/skills/public/sub-agent/SKILL.md +186 -0
- package/skills/public/sub-agent/references/security-review.md +153 -0
- package/skills/public/sub-agent/references/usage.md +207 -0
- package/skills/public/sub-agent/scripts/list_subagent_models.sh +22 -0
- package/skills/public/test-driven-development/SKILL.md +371 -0
- package/skills/public/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/public/webapp-testing/LICENSE.txt +202 -0
- package/skills/public/webapp-testing/SKILL.md +96 -0
- package/skills/public/webapp-testing/examples/console_logging.py +35 -0
- package/skills/public/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/public/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/public/webapp-testing/scripts/with_server.py +106 -0
- package/skills/public/xlsx/LICENSE.txt +30 -0
- package/skills/public/xlsx/SKILL.md +316 -0
- package/skills/public/xlsx/preview_data.py +93 -0
- package/skills/public/xlsx/recalc.py +178 -0
- package/tests/README.md +42 -0
- package/tests/fixtures/cli/claude_v0.9.2.0_argv.json +46 -0
- package/tests/fixtures/cli/claude_v0.9.2.0_stdout.json +32 -0
- package/tests/fixtures/cli/codex_run.jsonl +4 -0
- package/tests/fixtures/cli/opencode_run.jsonl +6 -0
- package/tests/integration/README.md +56 -0
- package/tests/integration/conftest.py +280 -0
- package/tests/integration/pytest.ini +13 -0
- package/tests/integration/test_mcp_auth.py +85 -0
- package/tests/integration/test_mcp_tools.py +101 -0
- package/tests/integration/test_workspace_lifecycle.py +125 -0
- package/tests/orchestrator/mock_llm_server.py +343 -0
- package/tests/orchestrator/test_cli_adapters.py +566 -0
- package/tests/orchestrator/test_cli_adapters_live.py +527 -0
- package/tests/orchestrator/test_cli_runtime.py +451 -0
- package/tests/orchestrator/test_docker_manager.py +302 -0
- package/tests/orchestrator/test_dynamic_instructions.py +69 -0
- package/tests/orchestrator/test_mcp_resources.py +140 -0
- package/tests/orchestrator/test_mcp_tools.py +224 -0
- package/tests/orchestrator/test_passthrough_isolation.py +201 -0
- package/tests/orchestrator/test_readme_in_container.py +76 -0
- package/tests/orchestrator/test_render_cache.py +84 -0
- package/tests/orchestrator/test_runtime_cli_endpoint.py +108 -0
- package/tests/orchestrator/test_single_user_mode.py +212 -0
- package/tests/orchestrator/test_startup_warnings.py +123 -0
- package/tests/orchestrator/test_sub_agent_dispatch.py +327 -0
- package/tests/orchestrator/test_subagent_claude_compat.py +367 -0
- package/tests/orchestrator/test_system_prompt_endpoint.py +191 -0
- package/tests/orchestrator/test_tool_descriptions.py +52 -0
- package/tests/orchestrator/test_view_image.py +201 -0
- package/tests/patches/conftest.py +30 -0
- package/tests/patches/fixtures/__init__.py +10 -0
- package/tests/patches/fixtures/middleware_v0.9.1.py +5057 -0
- package/tests/patches/fixtures/middleware_v0.9.2.py +5120 -0
- package/tests/patches/fixtures/retrieval_v0.9.1.py +2684 -0
- package/tests/patches/fixtures/retrieval_v0.9.2.py +2700 -0
- package/tests/patches/test_fix_attached_files_position.py +118 -0
- package/tests/patches/test_fix_large_tool_args.py +130 -0
- package/tests/patches/test_fix_large_tool_results.py +531 -0
- package/tests/patches/test_fix_skip_embedding_chat_files.py +160 -0
- package/tests/patches/test_fix_skip_rag_files_native_fc.py +120 -0
- package/tests/patches/test_fix_tool_loop_errors.py +128 -0
- package/tests/security/test_path_traversal_app.py +132 -0
- package/tests/security/test_path_traversal_docker.py +36 -0
- package/tests/security/test_path_traversal_settings.py +87 -0
- package/tests/security/test_safe_path_util.py +166 -0
- package/tests/security/test_xss_preview.py +46 -0
- package/tests/test-default-model-resolution.py +136 -0
- package/tests/test-docker-image.sh +358 -0
- package/tests/test-list-subagent-models.sh +421 -0
- package/tests/test-mcp-endpoint-live.sh +92 -0
- package/tests/test-mcp-native-surface.sh +213 -0
- package/tests/test-no-cyrillic.sh +135 -0
- package/tests/test-opencode-error-mapping.py +130 -0
- package/tests/test-pr88-skills.sh +305 -0
- package/tests/test-project-structure.sh +202 -0
- package/tests/test-single-user-mode.sh +269 -0
- package/tests/test-skill-no-hardcoded-models.sh +65 -0
- package/tests/test-subagent-cli-surface.py +137 -0
- package/tests/test-subagent-runtime.sh +109 -0
- package/tests/test_codex_toml_converter.py +204 -0
- package/tests/test_default_resolver_no_legacy_global.py +159 -0
- package/tests/test_filter.py +648 -0
- package/tests/test_init_sh_unchanged.sh +49 -0
- package/tests/test_opencode_alias_map_drop.py +144 -0
- package/tests/test_requirements.py +91 -0
- package/tests/test_subagent_docstring.py +193 -0
- package/tests/test_tools.py +34 -0
- package/vendor/extract-text/README.md +46 -0
- package/vendor/extract-text/extract-text +0 -0
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
# SPDX-License-Identifier: FSL-1.1-Apache-2.0
|
|
2
|
+
# Copyright (c) 2025 Open Computer Use Contributors
|
|
3
|
+
"""
|
|
4
|
+
Docker container management for Computer Use.
|
|
5
|
+
|
|
6
|
+
Handles:
|
|
7
|
+
- Docker client initialization (local socket)
|
|
8
|
+
- Container lifecycle (get/create/start)
|
|
9
|
+
- Network management (compose network, CDP proxy)
|
|
10
|
+
- Command execution (bash, python with stdin)
|
|
11
|
+
- Shutdown timer (idle timeout)
|
|
12
|
+
|
|
13
|
+
Extracted from mcp_tools.py to reduce file size and separate concerns.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import re
|
|
19
|
+
import json
|
|
20
|
+
import shlex
|
|
21
|
+
import time
|
|
22
|
+
import datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from types import SimpleNamespace
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
import aiohttp
|
|
28
|
+
import docker
|
|
29
|
+
from docker.utils.socket import frames_iter, demux_adaptor, consume_socket_output
|
|
30
|
+
|
|
31
|
+
import skill_manager
|
|
32
|
+
from context_vars import (
|
|
33
|
+
current_chat_id, current_user_email, current_user_name,
|
|
34
|
+
current_gitlab_token, current_gitlab_host,
|
|
35
|
+
current_anthropic_auth_token, current_anthropic_base_url,
|
|
36
|
+
current_mcp_tokens_url, current_mcp_tokens_api_key, current_mcp_servers,
|
|
37
|
+
)
|
|
38
|
+
from system_prompt import render_system_prompt_sync
|
|
39
|
+
|
|
40
|
+
DOCKER_SOCKET = os.getenv("DOCKER_SOCKET", "unix:///var/run/docker.sock")
|
|
41
|
+
DOCKER_IMAGE = os.getenv("DOCKER_IMAGE", "open-computer-use:latest")
|
|
42
|
+
CONTAINER_MEM_LIMIT = os.getenv("CONTAINER_MEM_LIMIT", "2g")
|
|
43
|
+
CONTAINER_CPU_LIMIT = float(os.getenv("CONTAINER_CPU_LIMIT", "1.0"))
|
|
44
|
+
COMMAND_TIMEOUT = int(os.getenv("COMMAND_TIMEOUT", "120"))
|
|
45
|
+
ENABLE_NETWORK = os.getenv("ENABLE_NETWORK", "true").lower() == "true"
|
|
46
|
+
USER_DATA_BASE_PATH = os.getenv("USER_DATA_BASE_PATH", "/tmp/computer-use-data")
|
|
47
|
+
# Public URL of the orchestrator — the single source of truth for browser-facing
|
|
48
|
+
# preview/archive links. Baked into /system-prompt so the model writes correct
|
|
49
|
+
# clickable URLs, and returned to the Open WebUI filter via the X-Public-Base-URL
|
|
50
|
+
# response header so outlet() decorations also use it.
|
|
51
|
+
#
|
|
52
|
+
# Internal-DNS default is only reachable from inside the compose network. Users
|
|
53
|
+
# must override with a browser-reachable URL (http://localhost:8081 for local
|
|
54
|
+
# dev, https://cu.example.com for prod) for the preview panel to work.
|
|
55
|
+
# See docs/openwebui-filter.md.
|
|
56
|
+
PUBLIC_BASE_URL_DEFAULT = "http://computer-use-server:8081"
|
|
57
|
+
# Normalize: treat empty string as unset (docker-compose's `${VAR:-}` always sets
|
|
58
|
+
# the env var, so os.getenv's default only fires when VAR is truly absent —
|
|
59
|
+
# empty string would otherwise bypass the startup warning). Also strip any
|
|
60
|
+
# trailing slash so downstream concatenations never produce `//files/...`.
|
|
61
|
+
PUBLIC_BASE_URL = (os.getenv("PUBLIC_BASE_URL") or PUBLIC_BASE_URL_DEFAULT).rstrip("/")
|
|
62
|
+
CONTAINER_IDLE_TIMEOUT = int(os.getenv("CONTAINER_IDLE_TIMEOUT", "600"))
|
|
63
|
+
DEBUG_LOGGING = os.getenv("DEBUG_LOGGING", "false").lower() == "true"
|
|
64
|
+
ORCHESTRATOR_CONTAINER_NAME = os.getenv("ORCHESTRATOR_CONTAINER_NAME", "computer-use-server")
|
|
65
|
+
BASE_DATA_DIR = Path(os.getenv("BASE_DATA_DIR", "/data"))
|
|
66
|
+
|
|
67
|
+
# MCP Tokens Wrapper for GitLab token fetching
|
|
68
|
+
MCP_TOKENS_URL = os.getenv("MCP_TOKENS_URL", "")
|
|
69
|
+
MCP_TOKENS_API_KEY = os.getenv("MCP_TOKENS_API_KEY", "")
|
|
70
|
+
|
|
71
|
+
# Sub-agent configuration — per-CLI default models (D-03/D-04).
|
|
72
|
+
# The legacy single SUB_AGENT_DEFAULT_MODEL global was removed in Phase 2;
|
|
73
|
+
# the deprecation grace window from Phase 1 D-10 is over. The per-CLI env
|
|
74
|
+
# vars (CLAUDE_/CODEX_/OPENCODE_SUB_AGENT_DEFAULT_MODEL) are read directly
|
|
75
|
+
# by the resolver in cli_runtime.py — no module-level constants needed
|
|
76
|
+
# here. The resolver raises a clear ValueError when caller passes no model
|
|
77
|
+
# AND the per-CLI env is unset (opencode/codex only; claude falls back to
|
|
78
|
+
# the canonical 'sonnet' alias).
|
|
79
|
+
SUB_AGENT_MAX_TURNS = int(os.getenv("SUB_AGENT_MAX_TURNS", "25"))
|
|
80
|
+
SUB_AGENT_TIMEOUT = int(os.getenv("SUB_AGENT_TIMEOUT", "3600"))
|
|
81
|
+
|
|
82
|
+
# Anthropic API (shared LiteLLM proxy key — fallback when no header provided)
|
|
83
|
+
# NB: os.getenv falls back to the default only when the var is UNSET. In docker
|
|
84
|
+
# compose with `${VAR:-}` the var is always set to "", so treat empty == unset.
|
|
85
|
+
ANTHROPIC_AUTH_TOKEN = os.getenv("ANTHROPIC_AUTH_TOKEN", "")
|
|
86
|
+
ANTHROPIC_BASE_URL = os.getenv("ANTHROPIC_BASE_URL") or "https://api.anthropic.com"
|
|
87
|
+
|
|
88
|
+
# Claude Code model ID overrides (pass through only when set on host — GATEWAY-02)
|
|
89
|
+
ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "")
|
|
90
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL = os.getenv("ANTHROPIC_DEFAULT_SONNET_MODEL", "")
|
|
91
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL = os.getenv("ANTHROPIC_DEFAULT_OPUS_MODEL", "")
|
|
92
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL = os.getenv("ANTHROPIC_DEFAULT_HAIKU_MODEL", "")
|
|
93
|
+
CLAUDE_CODE_SUBAGENT_MODEL = os.getenv("CLAUDE_CODE_SUBAGENT_MODEL", "")
|
|
94
|
+
# Claude Code gateway compatibility flags (set to "1" to disable — GATEWAY-02)
|
|
95
|
+
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = os.getenv("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", "")
|
|
96
|
+
DISABLE_PROMPT_CACHING = os.getenv("DISABLE_PROMPT_CACHING", "")
|
|
97
|
+
DISABLE_PROMPT_CACHING_SONNET = os.getenv("DISABLE_PROMPT_CACHING_SONNET", "")
|
|
98
|
+
DISABLE_PROMPT_CACHING_OPUS = os.getenv("DISABLE_PROMPT_CACHING_OPUS", "")
|
|
99
|
+
DISABLE_PROMPT_CACHING_HAIKU = os.getenv("DISABLE_PROMPT_CACHING_HAIKU", "")
|
|
100
|
+
|
|
101
|
+
# Tuple (not dict) for deterministic iteration order in tests — GATEWAY-03.
|
|
102
|
+
CLAUDE_CODE_PASSTHROUGH_ENVS = (
|
|
103
|
+
("ANTHROPIC_MODEL", ANTHROPIC_MODEL),
|
|
104
|
+
("ANTHROPIC_DEFAULT_SONNET_MODEL", ANTHROPIC_DEFAULT_SONNET_MODEL),
|
|
105
|
+
("ANTHROPIC_DEFAULT_OPUS_MODEL", ANTHROPIC_DEFAULT_OPUS_MODEL),
|
|
106
|
+
("ANTHROPIC_DEFAULT_HAIKU_MODEL", ANTHROPIC_DEFAULT_HAIKU_MODEL),
|
|
107
|
+
("CLAUDE_CODE_SUBAGENT_MODEL", CLAUDE_CODE_SUBAGENT_MODEL),
|
|
108
|
+
("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS),
|
|
109
|
+
("DISABLE_PROMPT_CACHING", DISABLE_PROMPT_CACHING),
|
|
110
|
+
("DISABLE_PROMPT_CACHING_SONNET", DISABLE_PROMPT_CACHING_SONNET),
|
|
111
|
+
("DISABLE_PROMPT_CACHING_OPUS", DISABLE_PROMPT_CACHING_OPUS),
|
|
112
|
+
("DISABLE_PROMPT_CACHING_HAIKU", DISABLE_PROMPT_CACHING_HAIKU),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Codex passthrough envs (Phase 6 — only injected when SUBAGENT_CLI=codex).
|
|
116
|
+
# Per AUTH-01 — closes Pitfall 1 (auth bleed across CLIs).
|
|
117
|
+
CODEX_PASSTHROUGH_ENVS = (
|
|
118
|
+
("OPENAI_API_KEY", os.getenv("OPENAI_API_KEY", "")),
|
|
119
|
+
("OPENAI_BASE_URL", os.getenv("OPENAI_BASE_URL", "")),
|
|
120
|
+
("CODEX_MODEL", os.getenv("CODEX_MODEL", "")),
|
|
121
|
+
("AZURE_OPENAI_API_KEY", os.getenv("AZURE_OPENAI_API_KEY", "")),
|
|
122
|
+
("AZURE_OPENAI_ENDPOINT", os.getenv("AZURE_OPENAI_ENDPOINT", "")),
|
|
123
|
+
("AZURE_OPENAI_API_VERSION", os.getenv("AZURE_OPENAI_API_VERSION", "")),
|
|
124
|
+
# Operator-supplied codex config override (see docs/cli-config-templates.md
|
|
125
|
+
# "Codex — custom OpenAI-compat gateway" recipe). Appended to the canonical
|
|
126
|
+
# ~/.codex/config.toml block by the Dockerfile entrypoint when set, so
|
|
127
|
+
# operators can route codex through a self-hosted gateway without forking.
|
|
128
|
+
# Without this entry the override never crosses the orchestrator → sandbox
|
|
129
|
+
# boundary. Same gap as #77; included here for codex parity.
|
|
130
|
+
("CODEX_CONFIG_EXTRA", os.getenv("CODEX_CONFIG_EXTRA", "")),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# OpenCode passthrough envs (Phase 6 — only injected when SUBAGENT_CLI=opencode).
|
|
134
|
+
# Includes OPENAI_API_KEY and ANTHROPIC_API_KEY because OpenCode itself supports
|
|
135
|
+
# multiple providers; the allowlist is per-CLI, not per-provider.
|
|
136
|
+
OPENCODE_PASSTHROUGH_ENVS = (
|
|
137
|
+
("OPENROUTER_API_KEY", os.getenv("OPENROUTER_API_KEY", "")),
|
|
138
|
+
("OPENAI_API_KEY", os.getenv("OPENAI_API_KEY", "")),
|
|
139
|
+
("ANTHROPIC_API_KEY", os.getenv("ANTHROPIC_API_KEY", "")),
|
|
140
|
+
("OPENCODE_MODEL", os.getenv("OPENCODE_MODEL", "")),
|
|
141
|
+
# Operator-supplied OpenCode config override (see docs/cli-config-templates.md
|
|
142
|
+
# "OpenCode — custom OpenAI-compat provider" recipe). Replaces /tmp/opencode.json
|
|
143
|
+
# verbatim when set, so operators can route the opencode sub-agent through a
|
|
144
|
+
# self-hosted gateway (LiteLLM, OpenLLM, etc.) for proxy-only deployments.
|
|
145
|
+
# Without this entry the override never crosses the orchestrator → sandbox
|
|
146
|
+
# boundary and the entrypoint heredoc falls through to the canonical
|
|
147
|
+
# 3-provider default. Closes #77.
|
|
148
|
+
("OPENCODE_CONFIG_EXTRA", os.getenv("OPENCODE_CONFIG_EXTRA", "")),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Sub-agent CLI runtime selector (CLI-01, CLI-02). Read once at module load
|
|
152
|
+
# and propagated to every spawned container via extra_env (D5 shape a).
|
|
153
|
+
# Empty/unset → "claude" (backwards-compat invariant). Invalid value → hard
|
|
154
|
+
# fail at module load (D1) so a typo in .env is visible in the very first
|
|
155
|
+
# `docker compose up` log line, never silently runs the wrong CLI.
|
|
156
|
+
_ALLOWED_CLIS = {"claude", "codex", "opencode"}
|
|
157
|
+
_raw_subagent_cli = os.getenv("SUBAGENT_CLI", "").strip().lower()
|
|
158
|
+
if _raw_subagent_cli and _raw_subagent_cli not in _ALLOWED_CLIS:
|
|
159
|
+
print(
|
|
160
|
+
f"[computer-use-server] FATAL: SUBAGENT_CLI={_raw_subagent_cli!r} "
|
|
161
|
+
f"is not one of {{claude, codex, opencode}}.",
|
|
162
|
+
file=sys.stderr,
|
|
163
|
+
)
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
SUBAGENT_CLI = _raw_subagent_cli or "claude"
|
|
166
|
+
|
|
167
|
+
# Active passthrough set selected by SUBAGENT_CLI — AUTH-01 / Pitfall 1.
|
|
168
|
+
# Single source of truth for "which auth env vars cross the orchestrator->sandbox
|
|
169
|
+
# boundary for this runtime". `_create_container` reads this once per container.
|
|
170
|
+
_PASSTHROUGH_BY_CLI = {
|
|
171
|
+
"claude": CLAUDE_CODE_PASSTHROUGH_ENVS,
|
|
172
|
+
"codex": CODEX_PASSTHROUGH_ENVS,
|
|
173
|
+
"opencode": OPENCODE_PASSTHROUGH_ENVS,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Vision API for describe-image / upd-processing skills
|
|
177
|
+
VISION_API_KEY = os.getenv("VISION_API_KEY", "")
|
|
178
|
+
VISION_API_URL = os.getenv("VISION_API_URL", "")
|
|
179
|
+
VISION_MODEL = os.getenv("VISION_MODEL", "gpt-4o")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def warn_if_public_base_url_is_default() -> bool:
|
|
183
|
+
"""Emit a one-time startup warning when PUBLIC_BASE_URL is still the
|
|
184
|
+
hardcoded internal-DNS default.
|
|
185
|
+
|
|
186
|
+
The default (http://computer-use-server:8081) is only reachable from inside
|
|
187
|
+
the compose network. Since the public URL is now baked into /system-prompt
|
|
188
|
+
and returned to the filter via response header, a default value means the
|
|
189
|
+
preview panel will never appear — the browser cannot resolve the internal
|
|
190
|
+
DNS name.
|
|
191
|
+
|
|
192
|
+
Returns True if a warning was emitted (useful for tests), False otherwise.
|
|
193
|
+
Called once from FastAPI lifespan startup — do not call per-request.
|
|
194
|
+
"""
|
|
195
|
+
if PUBLIC_BASE_URL == PUBLIC_BASE_URL_DEFAULT:
|
|
196
|
+
print(
|
|
197
|
+
"[computer-use-server] WARNING: PUBLIC_BASE_URL is still the "
|
|
198
|
+
f"hardcoded default ({PUBLIC_BASE_URL_DEFAULT!r}). This URL is only "
|
|
199
|
+
"reachable from inside the compose network — the Open WebUI preview "
|
|
200
|
+
"panel will never appear until you set it to a browser-reachable URL.\n"
|
|
201
|
+
" Fix: in .env, set PUBLIC_BASE_URL=http://<browser-reachable-host>:8081.\n"
|
|
202
|
+
" Docs: https://github.com/Wide-Moat/open-computer-use/blob/main/docs/openwebui-filter.md"
|
|
203
|
+
)
|
|
204
|
+
return True
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def warn_if_mcp_api_key_missing() -> bool:
|
|
209
|
+
"""Emit a one-time startup warning when MCP_API_KEY is empty.
|
|
210
|
+
|
|
211
|
+
An empty MCP_API_KEY makes every /mcp endpoint publicly callable without
|
|
212
|
+
authentication — fine for local dev, dangerous for any deployment the
|
|
213
|
+
internet can reach. Warn loudly so the condition does not silently survive
|
|
214
|
+
a prod rollout.
|
|
215
|
+
|
|
216
|
+
Returns True if a warning was emitted (useful for tests), False otherwise.
|
|
217
|
+
Called once from FastAPI lifespan startup — do not call per-request.
|
|
218
|
+
"""
|
|
219
|
+
if not os.getenv("MCP_API_KEY", ""):
|
|
220
|
+
print(
|
|
221
|
+
"[computer-use-server] WARNING: MCP_API_KEY is empty — the /mcp "
|
|
222
|
+
"endpoints accept ANY caller with no auth. Acceptable for local "
|
|
223
|
+
"development, unsafe for anything reachable from the internet.\n"
|
|
224
|
+
" Fix: set MCP_API_KEY in .env to a long random string and mirror "
|
|
225
|
+
"it in the Open WebUI tool Valve (Admin → Tools → Computer Use → "
|
|
226
|
+
"Valves → MCP_API_KEY)."
|
|
227
|
+
)
|
|
228
|
+
return True
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def warn_subagent_cli() -> bool:
|
|
233
|
+
"""Emit a one-line banner naming the active sub-agent CLI runtime.
|
|
234
|
+
|
|
235
|
+
Always prints (informational, not gated on a default) so operators have
|
|
236
|
+
visible confirmation that SUBAGENT_CLI took effect after a docker compose
|
|
237
|
+
restart. Mirrors warn_if_public_base_url_is_default's bool-return
|
|
238
|
+
contract so app.py lifespan can collect emission flags for future
|
|
239
|
+
telemetry. Closes the UX gap from PITFALLS.md UX table row 1.
|
|
240
|
+
|
|
241
|
+
Returns True (always emitted, kept for symmetry with sibling warn_*).
|
|
242
|
+
Called once from FastAPI lifespan startup — do not call per-request.
|
|
243
|
+
"""
|
|
244
|
+
print(f"[MCP] Sub-agent runtime: {SUBAGENT_CLI}")
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def _fetch_gitlab_token(email: str, mcp_tokens_url: str, mcp_tokens_api_key: str) -> Optional[str]:
|
|
249
|
+
"""
|
|
250
|
+
Fetch decrypted GitLab token from MCP Tokens Wrapper service.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
email: User email address
|
|
254
|
+
mcp_tokens_url: URL of MCP Tokens Wrapper service
|
|
255
|
+
mcp_tokens_api_key: Internal API key for authentication
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
GitLab token string or None if not found/error
|
|
259
|
+
"""
|
|
260
|
+
if not mcp_tokens_api_key:
|
|
261
|
+
print("[GITLAB] MCP_TOKENS_API_KEY not configured, skipping token fetch")
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
if not email:
|
|
265
|
+
print("[GITLAB] No email provided, skipping token fetch")
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
url = f"{mcp_tokens_url}/api/internal/tokens/{email}/gitlab"
|
|
269
|
+
headers = {"X-Internal-Api-Key": mcp_tokens_api_key}
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
async with aiohttp.ClientSession() as session:
|
|
273
|
+
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=5)) as response:
|
|
274
|
+
if response.status == 200:
|
|
275
|
+
data = await response.json()
|
|
276
|
+
token = data.get("token")
|
|
277
|
+
if token:
|
|
278
|
+
print(f"[GITLAB] Token fetched for {email}")
|
|
279
|
+
return token
|
|
280
|
+
elif response.status == 404:
|
|
281
|
+
print(f"[GITLAB] No token found for {email}")
|
|
282
|
+
else:
|
|
283
|
+
print(f"[GITLAB] Error fetching token: HTTP {response.status}")
|
|
284
|
+
except asyncio.TimeoutError:
|
|
285
|
+
print(f"[GITLAB] Timeout fetching token for {email}")
|
|
286
|
+
except Exception as e:
|
|
287
|
+
print(f"[GITLAB] Error fetching token: {e}")
|
|
288
|
+
|
|
289
|
+
async def _ensure_gitlab_token():
|
|
290
|
+
"""
|
|
291
|
+
Ensure GitLab token is available, fetching from MCP Tokens Wrapper if needed.
|
|
292
|
+
|
|
293
|
+
Priority:
|
|
294
|
+
1. Token from header (current_gitlab_token already set)
|
|
295
|
+
2. Fetch from MCP Tokens Wrapper by user email
|
|
296
|
+
3. No token (continue without GitLab auth)
|
|
297
|
+
"""
|
|
298
|
+
# If token already set from header, use it
|
|
299
|
+
if current_gitlab_token.get():
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
# Try to fetch from MCP Tokens Wrapper
|
|
303
|
+
user_email = current_user_email.get()
|
|
304
|
+
mcp_tokens_url = current_mcp_tokens_url.get() or MCP_TOKENS_URL
|
|
305
|
+
mcp_tokens_api_key = current_mcp_tokens_api_key.get() or MCP_TOKENS_API_KEY
|
|
306
|
+
|
|
307
|
+
if user_email and mcp_tokens_url and mcp_tokens_api_key:
|
|
308
|
+
token = await _fetch_gitlab_token(user_email, mcp_tokens_url, mcp_tokens_api_key)
|
|
309
|
+
if token:
|
|
310
|
+
current_gitlab_token.set(token)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# Global Docker client (lazy init)
|
|
315
|
+
_docker_client: Optional[docker.DockerClient] = None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def build_mcp_config(server_names_csv: str, base_url: Optional[str], user_email: str = "") -> dict | None:
|
|
320
|
+
"""Build Claude Code ~/.mcp.json config from comma-separated server names.
|
|
321
|
+
|
|
322
|
+
URLs are templated as {base_url}/mcp/{server_name} (LiteLLM MCP proxy pattern).
|
|
323
|
+
Authorization uses ANTHROPIC_AUTH_TOKEN env var (resolved inside container at write time).
|
|
324
|
+
|
|
325
|
+
Returns dict ready for json.dumps, or None if no servers specified.
|
|
326
|
+
|
|
327
|
+
``base_url`` may be None or empty; both fall back to the module-level
|
|
328
|
+
ANTHROPIC_BASE_URL constant so callers can pass the ContextVar value
|
|
329
|
+
directly without a manual fallback.
|
|
330
|
+
"""
|
|
331
|
+
# Blocklist: prevent recursive sub_agent loops
|
|
332
|
+
BLOCKED_SERVERS = {"docker_ai", "docker-ai"}
|
|
333
|
+
|
|
334
|
+
names = [s.strip() for s in server_names_csv.split(",") if s.strip() and s.strip() not in BLOCKED_SERVERS]
|
|
335
|
+
if not names:
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
base = (base_url or ANTHROPIC_BASE_URL or "https://api.anthropic.com").rstrip("/")
|
|
339
|
+
servers = {}
|
|
340
|
+
for name in names:
|
|
341
|
+
servers[name] = {
|
|
342
|
+
"type": "http",
|
|
343
|
+
"url": f"{base}/mcp/{name}",
|
|
344
|
+
"headers": {
|
|
345
|
+
"x-openwebui-user-email": user_email,
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
return {"mcpServers": servers}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def build_mcp_config_write_script(mcp_config: dict) -> str:
|
|
352
|
+
"""Build a shell command that writes ~/.mcp.json inside a container.
|
|
353
|
+
|
|
354
|
+
ANTHROPIC_AUTH_TOKEN is resolved from the container's env at runtime,
|
|
355
|
+
so no secrets are baked into the script itself.
|
|
356
|
+
Uses base64 to avoid shell/JSON escaping issues.
|
|
357
|
+
"""
|
|
358
|
+
import base64
|
|
359
|
+
config_b64 = base64.b64encode(json.dumps(mcp_config).encode()).decode()
|
|
360
|
+
return (
|
|
361
|
+
f"python3 -c '"
|
|
362
|
+
f"import json,os,base64;"
|
|
363
|
+
f"c=json.loads(base64.b64decode(\"{config_b64}\"));"
|
|
364
|
+
f"k=os.environ.get(\"ANTHROPIC_AUTH_TOKEN\",\"\");"
|
|
365
|
+
f"[s[\"headers\"].__setitem__(\"Authorization\",\"Bearer \"+k)"
|
|
366
|
+
f" for s in c[\"mcpServers\"].values() if \"headers\" in s];"
|
|
367
|
+
f"json.dump(c,open(os.path.expanduser(\"~/.mcp.json\"),\"w\"),indent=2);"
|
|
368
|
+
# Auto-approve MCP servers in settings.local.json so Claude Code doesn't ask
|
|
369
|
+
f"p=os.path.expanduser(\"~/.claude/settings.local.json\");"
|
|
370
|
+
f"sl=json.load(open(p)) if os.path.exists(p) else {{}};"
|
|
371
|
+
f"sl[\"enabledMcpjsonServers\"]=list(c[\"mcpServers\"].keys());"
|
|
372
|
+
f"json.dump(sl,open(p,\"w\"),indent=2)"
|
|
373
|
+
f"'"
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def get_docker_client() -> docker.DockerClient:
|
|
378
|
+
"""Get or create Docker client connected to local socket."""
|
|
379
|
+
global _docker_client
|
|
380
|
+
if _docker_client is None:
|
|
381
|
+
_docker_client = docker.DockerClient(base_url=DOCKER_SOCKET)
|
|
382
|
+
_docker_client.ping()
|
|
383
|
+
print(f"[MCP] Connected to Docker at {DOCKER_SOCKET}")
|
|
384
|
+
return _docker_client
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _build_container_env(extra_env: Optional[dict] = None) -> dict:
|
|
388
|
+
"""Build environment variables dict for container."""
|
|
389
|
+
env = {
|
|
390
|
+
"NPM_CONFIG_PREFIX": "/usr/local/lib/node_modules_global",
|
|
391
|
+
}
|
|
392
|
+
if extra_env:
|
|
393
|
+
env.update(extra_env)
|
|
394
|
+
return env
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# Cached compose network name (detected once, reused)
|
|
398
|
+
_compose_network_name: Optional[str] = None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _get_compose_network_name(force_refresh: bool = False) -> Optional[str]:
|
|
402
|
+
"""Find the Docker compose network that computer-use-orchestrator is on (for CDP proxy access)."""
|
|
403
|
+
global _compose_network_name
|
|
404
|
+
if _compose_network_name is not None and not force_refresh:
|
|
405
|
+
return _compose_network_name
|
|
406
|
+
|
|
407
|
+
client = get_docker_client()
|
|
408
|
+
try:
|
|
409
|
+
fs = client.containers.get(ORCHESTRATOR_CONTAINER_NAME)
|
|
410
|
+
fs.reload()
|
|
411
|
+
for name in fs.attrs["NetworkSettings"]["Networks"]:
|
|
412
|
+
if name != "bridge":
|
|
413
|
+
_compose_network_name = name
|
|
414
|
+
print(f"[MCP] Detected compose network: {name}")
|
|
415
|
+
return name
|
|
416
|
+
except Exception as e:
|
|
417
|
+
print(f"[MCP] Could not detect compose network: {e}")
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def get_container_cdp_address(chat_id: str) -> Optional[str]:
|
|
422
|
+
"""Get the IP address of a chat's container on the compose network (for CDP proxy).
|
|
423
|
+
|
|
424
|
+
After deploy (docker-compose down/up), running containers may still be on the
|
|
425
|
+
old compose network with an unreachable IP. This function detects the mismatch
|
|
426
|
+
and reconnects the container to the current compose network.
|
|
427
|
+
"""
|
|
428
|
+
chat_id = chat_id.lower()
|
|
429
|
+
client = get_docker_client()
|
|
430
|
+
sanitized_id = re.sub(r'[^a-zA-Z0-9_.-]', '-', chat_id)
|
|
431
|
+
container_name = f"owui-chat-{sanitized_id}"
|
|
432
|
+
try:
|
|
433
|
+
c = client.containers.get(container_name)
|
|
434
|
+
c.reload()
|
|
435
|
+
if c.status != "running":
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
compose_net = _get_compose_network_name()
|
|
439
|
+
networks = c.attrs["NetworkSettings"]["Networks"]
|
|
440
|
+
|
|
441
|
+
# If container is on the current compose network, use that IP
|
|
442
|
+
if compose_net and compose_net in networks:
|
|
443
|
+
ip = networks[compose_net].get("IPAddress")
|
|
444
|
+
if ip:
|
|
445
|
+
return ip
|
|
446
|
+
|
|
447
|
+
# Container running but NOT on compose network → fix and retry
|
|
448
|
+
if compose_net and compose_net not in networks:
|
|
449
|
+
print(f"[MCP] {container_name} not on compose network {compose_net}, reconnecting...")
|
|
450
|
+
_fix_dead_networks(client, c)
|
|
451
|
+
c.reload()
|
|
452
|
+
networks = c.attrs["NetworkSettings"]["Networks"]
|
|
453
|
+
if compose_net in networks:
|
|
454
|
+
ip = networks[compose_net].get("IPAddress")
|
|
455
|
+
if ip:
|
|
456
|
+
return ip
|
|
457
|
+
|
|
458
|
+
# Fallback: first non-bridge IP
|
|
459
|
+
for net_name, net_data in networks.items():
|
|
460
|
+
if net_name != "bridge" and net_data.get("IPAddress"):
|
|
461
|
+
return net_data["IPAddress"]
|
|
462
|
+
ip = c.attrs["NetworkSettings"]["IPAddress"]
|
|
463
|
+
return ip if ip else None
|
|
464
|
+
except Exception:
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _fix_dead_networks(client, container):
|
|
469
|
+
"""Disconnect from dead networks and reconnect to compose network.
|
|
470
|
+
|
|
471
|
+
After docker-compose down/up (deploy), networks are recreated with new IDs.
|
|
472
|
+
Stopped containers still reference old (dead) networks, causing start() to fail.
|
|
473
|
+
Same logic as restart-container endpoint in app.py.
|
|
474
|
+
"""
|
|
475
|
+
try:
|
|
476
|
+
container.reload()
|
|
477
|
+
old_nets = list(container.attrs.get("NetworkSettings", {}).get("Networks", {}).keys())
|
|
478
|
+
for net_name in old_nets:
|
|
479
|
+
try:
|
|
480
|
+
net = client.networks.get(net_name)
|
|
481
|
+
net.disconnect(container, force=True)
|
|
482
|
+
except Exception:
|
|
483
|
+
pass # Network already dead — ignore
|
|
484
|
+
compose_net = _get_compose_network_name(force_refresh=True)
|
|
485
|
+
if compose_net:
|
|
486
|
+
try:
|
|
487
|
+
net = client.networks.get(compose_net)
|
|
488
|
+
net.connect(container)
|
|
489
|
+
except Exception as e:
|
|
490
|
+
print(f"[MCP] Warning: could not connect to {compose_net}: {e}")
|
|
491
|
+
except Exception as e:
|
|
492
|
+
print(f"[MCP] Warning: network fix failed: {e}")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _get_or_create_container(chat_id: str) -> docker.models.containers.Container:
|
|
496
|
+
"""Get existing container or create new one for this chat."""
|
|
497
|
+
chat_id = chat_id.lower()
|
|
498
|
+
client = get_docker_client()
|
|
499
|
+
|
|
500
|
+
# Sanitize chat_id for Docker container naming
|
|
501
|
+
sanitized_id = re.sub(r'[^a-zA-Z0-9_.-]', '-', chat_id)
|
|
502
|
+
container_name = f"owui-chat-{sanitized_id}"
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
container = client.containers.get(container_name)
|
|
506
|
+
container.reload()
|
|
507
|
+
|
|
508
|
+
if container.status == "exited":
|
|
509
|
+
_fix_dead_networks(client, container)
|
|
510
|
+
container.start()
|
|
511
|
+
print(f"[MCP] Started existing container: {container_name}")
|
|
512
|
+
elif container.status == "running":
|
|
513
|
+
if DEBUG_LOGGING:
|
|
514
|
+
print(f"[MCP] Reusing running container: {container_name}")
|
|
515
|
+
else:
|
|
516
|
+
container.start()
|
|
517
|
+
print(f"[MCP] Started container in state '{container.status}': {container_name}")
|
|
518
|
+
|
|
519
|
+
return container
|
|
520
|
+
|
|
521
|
+
except docker.errors.NotFound:
|
|
522
|
+
print(f"[MCP] Creating new container: {container_name}")
|
|
523
|
+
return _create_container(chat_id, container_name)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _create_container(chat_id: str, container_name: str) -> docker.models.containers.Container:
|
|
527
|
+
"""Create a new persistent container for this chat."""
|
|
528
|
+
client = get_docker_client()
|
|
529
|
+
|
|
530
|
+
# Build extra env from context variables
|
|
531
|
+
extra_env = {
|
|
532
|
+
"GITLAB_HOST": current_gitlab_host.get(),
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
gitlab_token = current_gitlab_token.get()
|
|
536
|
+
if gitlab_token:
|
|
537
|
+
extra_env["GITLAB_TOKEN"] = gitlab_token
|
|
538
|
+
print(f"[MCP] Injecting GITLAB_TOKEN into container environment")
|
|
539
|
+
|
|
540
|
+
# Phase 3 gateway-path injection — only active when SUBAGENT_CLI=claude
|
|
541
|
+
# (AUTH-01: no Anthropic gateway vars bleed into codex/opencode containers).
|
|
542
|
+
if SUBAGENT_CLI == "claude":
|
|
543
|
+
anthropic_key = current_anthropic_auth_token.get() or ANTHROPIC_AUTH_TOKEN
|
|
544
|
+
anthropic_base = current_anthropic_base_url.get() or ANTHROPIC_BASE_URL
|
|
545
|
+
if anthropic_key:
|
|
546
|
+
extra_env["ANTHROPIC_AUTH_TOKEN"] = anthropic_key
|
|
547
|
+
extra_env["ANTHROPIC_BASE_URL"] = anthropic_base
|
|
548
|
+
|
|
549
|
+
# Inject only the active CLI's auth allowlist (AUTH-01 / Pitfall 1: no
|
|
550
|
+
# auth bleed across CLIs — e.g. when SUBAGENT_CLI=opencode, OPENAI_API_KEY
|
|
551
|
+
# and OPENROUTER_API_KEY land in extra_env but ANTHROPIC_* gateway vars
|
|
552
|
+
# do NOT, even if set on the host).
|
|
553
|
+
for _name, _value in _PASSTHROUGH_BY_CLI[SUBAGENT_CLI]:
|
|
554
|
+
if _value:
|
|
555
|
+
extra_env[_name] = _value
|
|
556
|
+
|
|
557
|
+
# Sub-agent runtime selector (CLI-01) — propagated to every container so
|
|
558
|
+
# the Phase 7 .bashrc autostart `exec "${SUBAGENT_CLI:-claude}"` can read it
|
|
559
|
+
# and `docker inspect <sandbox>` shows the chosen runtime in Env.
|
|
560
|
+
extra_env["SUBAGENT_CLI"] = SUBAGENT_CLI
|
|
561
|
+
|
|
562
|
+
# OpenCode reads its config from $OPENCODE_CONFIG. Pin it to /tmp so docker
|
|
563
|
+
# exec'd subprocesses (e.g. mcp_tools.sub_agent dispatch) inherit it — the
|
|
564
|
+
# entrypoint `export OPENCODE_CONFIG=/tmp/opencode.json` only affects the
|
|
565
|
+
# entrypoint shell session, NOT subsequent `docker exec` invocations.
|
|
566
|
+
# Without this pin, OpenCode would fall back to ~/.local/share/opencode/auth.json
|
|
567
|
+
# and reopen the Pitfall 7 leak vector. ROADMAP success #2: `docker inspect`
|
|
568
|
+
# must show this env in the container Env.
|
|
569
|
+
if SUBAGENT_CLI == "opencode":
|
|
570
|
+
extra_env["OPENCODE_CONFIG"] = "/tmp/opencode.json"
|
|
571
|
+
# Propagate request-scoped X-Anthropic-Api-Key into the env name OpenCode
|
|
572
|
+
# expects (`{env:ANTHROPIC_API_KEY}` per docs/multi-cli.md and the
|
|
573
|
+
# entrypoint heredoc in Dockerfile). Without this, header-authenticated
|
|
574
|
+
# runs lose their credential when SUBAGENT_CLI=opencode because the claude
|
|
575
|
+
# branch above is not active. Process-level ANTHROPIC_AUTH_TOKEN env is
|
|
576
|
+
# the host-level fallback (covered by OPENCODE_PASSTHROUGH_ENVS — but the
|
|
577
|
+
# request-scoped header path was missed). Per CodeRabbit PR#75 review.
|
|
578
|
+
request_scoped_anthropic = current_anthropic_auth_token.get()
|
|
579
|
+
if request_scoped_anthropic:
|
|
580
|
+
extra_env["ANTHROPIC_API_KEY"] = request_scoped_anthropic
|
|
581
|
+
|
|
582
|
+
# Vision API for describe-image / upd-processing skills
|
|
583
|
+
if VISION_API_KEY:
|
|
584
|
+
extra_env["VISION_API_KEY"] = VISION_API_KEY
|
|
585
|
+
extra_env["VISION_API_URL"] = VISION_API_URL
|
|
586
|
+
extra_env["VISION_MODEL"] = VISION_MODEL
|
|
587
|
+
|
|
588
|
+
user_name = current_user_name.get()
|
|
589
|
+
user_email = current_user_email.get()
|
|
590
|
+
if user_name:
|
|
591
|
+
extra_env["GIT_AUTHOR_NAME"] = user_name
|
|
592
|
+
extra_env["GIT_COMMITTER_NAME"] = user_name
|
|
593
|
+
if user_email:
|
|
594
|
+
extra_env["GIT_AUTHOR_EMAIL"] = user_email
|
|
595
|
+
extra_env["GIT_COMMITTER_EMAIL"] = user_email
|
|
596
|
+
# Anthropic-specific custom header — only emit for the claude runtime
|
|
597
|
+
# so codex / opencode containers do not get spurious anthropic env.
|
|
598
|
+
if SUBAGENT_CLI == "claude":
|
|
599
|
+
extra_env["ANTHROPIC_CUSTOM_HEADERS"] = f"x-openwebui-user-email: {user_email}"
|
|
600
|
+
|
|
601
|
+
# Workspace volume for this chat
|
|
602
|
+
workspace_volume = f"chat-{chat_id}-workspace"
|
|
603
|
+
|
|
604
|
+
# Host paths for user data
|
|
605
|
+
chat_data_path = os.path.join(USER_DATA_BASE_PATH, chat_id)
|
|
606
|
+
uploads_path = os.path.join(chat_data_path, "uploads")
|
|
607
|
+
outputs_path = os.path.join(chat_data_path, "outputs")
|
|
608
|
+
|
|
609
|
+
# Create directories on Docker host with correct permissions
|
|
610
|
+
try:
|
|
611
|
+
print(f"[MCP] Creating directories: {uploads_path}, {outputs_path}")
|
|
612
|
+
client.containers.run(
|
|
613
|
+
image=DOCKER_IMAGE,
|
|
614
|
+
command=f"bash -c 'mkdir -p {shlex.quote(uploads_path)} {shlex.quote(outputs_path)} && chmod -R 777 {shlex.quote(chat_data_path)}'",
|
|
615
|
+
volumes={"/tmp": {"bind": "/tmp", "mode": "rw"}},
|
|
616
|
+
remove=True,
|
|
617
|
+
detach=False,
|
|
618
|
+
user="root"
|
|
619
|
+
)
|
|
620
|
+
except Exception as e:
|
|
621
|
+
print(f"[MCP] Warning: Failed to create directories: {e}")
|
|
622
|
+
|
|
623
|
+
# Check if using custom image (has entrypoint) or standard image
|
|
624
|
+
use_entrypoint = "computer-use" in DOCKER_IMAGE or "open-computer-use" in DOCKER_IMAGE
|
|
625
|
+
|
|
626
|
+
if use_entrypoint:
|
|
627
|
+
# Production: use entrypoint script
|
|
628
|
+
command = ["bash", "-c", "/home/assistant/.entrypoint.sh bash -c 'trap \"exit 0\" SIGTERM SIGINT; tail -f /dev/null & wait $!'"]
|
|
629
|
+
working_dir = "/home/assistant"
|
|
630
|
+
user = "assistant:assistant"
|
|
631
|
+
else:
|
|
632
|
+
# Development/test: simple bash loop
|
|
633
|
+
command = ["bash", "-c", "trap 'exit 0' SIGTERM SIGINT; tail -f /dev/null & wait $!"]
|
|
634
|
+
working_dir = "/root"
|
|
635
|
+
user = None # Use image default
|
|
636
|
+
|
|
637
|
+
config = {
|
|
638
|
+
"image": DOCKER_IMAGE,
|
|
639
|
+
"name": container_name,
|
|
640
|
+
"hostname": f"chat-{chat_id[:8]}",
|
|
641
|
+
"command": command,
|
|
642
|
+
"detach": True,
|
|
643
|
+
"stdin_open": True,
|
|
644
|
+
"tty": True,
|
|
645
|
+
"mem_limit": CONTAINER_MEM_LIMIT,
|
|
646
|
+
"nano_cpus": int(CONTAINER_CPU_LIMIT * 1_000_000_000),
|
|
647
|
+
"working_dir": working_dir,
|
|
648
|
+
"environment": _build_container_env(extra_env),
|
|
649
|
+
"volumes": {
|
|
650
|
+
workspace_volume: {"bind": working_dir, "mode": "rw"},
|
|
651
|
+
uploads_path: {"bind": "/mnt/user-data/uploads", "mode": "ro"},
|
|
652
|
+
outputs_path: {"bind": "/mnt/user-data/outputs", "mode": "rw"},
|
|
653
|
+
**skill_manager.get_skill_mounts(
|
|
654
|
+
skill_manager.get_user_skills_sync(current_user_email.get())
|
|
655
|
+
),
|
|
656
|
+
},
|
|
657
|
+
"labels": {
|
|
658
|
+
"managed-by": "mcp-computer-use-orchestrator",
|
|
659
|
+
"chat-id": chat_id,
|
|
660
|
+
"tool": "computer-use-mcp"
|
|
661
|
+
},
|
|
662
|
+
"security_opt": ["no-new-privileges:true"],
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if user:
|
|
666
|
+
config["user"] = user
|
|
667
|
+
|
|
668
|
+
if not ENABLE_NETWORK:
|
|
669
|
+
config["network_disabled"] = True
|
|
670
|
+
|
|
671
|
+
try:
|
|
672
|
+
container = client.containers.create(**config)
|
|
673
|
+
except docker.errors.APIError as e:
|
|
674
|
+
if e.status_code == 409:
|
|
675
|
+
# Container name exists (stale after deploy) — remove and retry
|
|
676
|
+
print(f"[MCP] [409] Removing stale container: {container_name}")
|
|
677
|
+
try:
|
|
678
|
+
old = client.containers.get(container_name)
|
|
679
|
+
old.remove(force=True)
|
|
680
|
+
except Exception:
|
|
681
|
+
pass
|
|
682
|
+
container = client.containers.create(**config)
|
|
683
|
+
else:
|
|
684
|
+
raise
|
|
685
|
+
container.start()
|
|
686
|
+
|
|
687
|
+
# Connect to compose network so computer-use-orchestrator can proxy CDP (port 9222) to this container
|
|
688
|
+
try:
|
|
689
|
+
compose_net = _get_compose_network_name()
|
|
690
|
+
if compose_net:
|
|
691
|
+
client.networks.get(compose_net).connect(container)
|
|
692
|
+
if DEBUG_LOGGING:
|
|
693
|
+
print(f"[MCP] Connected {container_name} to network {compose_net}")
|
|
694
|
+
except Exception as e:
|
|
695
|
+
print(f"[MCP] Warning: Could not connect to compose network: {e}")
|
|
696
|
+
|
|
697
|
+
print(f"[MCP] Created and started new container: {container_name}")
|
|
698
|
+
|
|
699
|
+
# Save metadata for resurrection after container removal by cron
|
|
700
|
+
save_container_meta(
|
|
701
|
+
chat_id,
|
|
702
|
+
current_user_email.get(),
|
|
703
|
+
current_user_name.get(),
|
|
704
|
+
current_mcp_servers.get(),
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
# Write MCP config on creation so terminal users have it immediately
|
|
708
|
+
try:
|
|
709
|
+
mcp_servers_str = current_mcp_servers.get()
|
|
710
|
+
if mcp_servers_str:
|
|
711
|
+
mcp_cfg = build_mcp_config(
|
|
712
|
+
mcp_servers_str,
|
|
713
|
+
current_anthropic_base_url.get(),
|
|
714
|
+
current_user_email.get() or "",
|
|
715
|
+
)
|
|
716
|
+
if mcp_cfg:
|
|
717
|
+
write_cmd = build_mcp_config_write_script(mcp_cfg)
|
|
718
|
+
_execute_bash(container, write_cmd, 15)
|
|
719
|
+
print(f"[MCP] Wrote MCP config on container creation: {mcp_servers_str}")
|
|
720
|
+
except Exception as e:
|
|
721
|
+
print(f"[MCP] Warning: MCP setup on create failed: {e}")
|
|
722
|
+
|
|
723
|
+
# Tier 2 — write /home/assistant/README.md with the rendered system prompt
|
|
724
|
+
# so the model can always recover its environment via `view` regardless of
|
|
725
|
+
# what the client did (or didn't do) with prompts/get and InitializeResult.
|
|
726
|
+
#
|
|
727
|
+
# Safe to call asyncio.run here: _create_container runs inside
|
|
728
|
+
# asyncio.to_thread (see all call sites in mcp_tools.py) → worker thread
|
|
729
|
+
# with no running event loop → no nested-loop error.
|
|
730
|
+
try:
|
|
731
|
+
_, workdir = _get_container_user_and_workdir()
|
|
732
|
+
readme_text = render_system_prompt_sync(chat_id, current_user_email.get())
|
|
733
|
+
_write_file_to_container(container, workdir, "README.md", readme_text)
|
|
734
|
+
print(f"[MCP] Wrote {workdir}/README.md ({len(readme_text)} chars)")
|
|
735
|
+
except Exception as e:
|
|
736
|
+
print(f"[MCP] Warning: README.md write failed: {e}")
|
|
737
|
+
|
|
738
|
+
# Tier 6 — initial sync of uploaded files into MCP resources registry.
|
|
739
|
+
# Lazy import to avoid circular (mcp_resources → mcp_tools → docker_manager).
|
|
740
|
+
try:
|
|
741
|
+
from mcp_resources import sync_chat_resources_sync
|
|
742
|
+
n = sync_chat_resources_sync(chat_id)
|
|
743
|
+
if n:
|
|
744
|
+
print(f"[MCP] Registered {n} upload resource(s) for chat {chat_id}")
|
|
745
|
+
except Exception as e:
|
|
746
|
+
print(f"[MCP] Warning: MCP resources sync failed: {e}")
|
|
747
|
+
|
|
748
|
+
# Pitfall 7 defense — scrub OpenCode auth.json from volume on container
|
|
749
|
+
# creation (handles resurrected containers from previous opencode-auth-login
|
|
750
|
+
# experiments). Best-effort — silent on failure (absence is normal).
|
|
751
|
+
try:
|
|
752
|
+
container.exec_run(
|
|
753
|
+
"rm -f /home/assistant/.local/share/opencode/auth.json",
|
|
754
|
+
user="assistant",
|
|
755
|
+
)
|
|
756
|
+
except Exception:
|
|
757
|
+
pass
|
|
758
|
+
|
|
759
|
+
return container
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _write_file_to_container(container, dirpath: str, filename: str, text: str) -> None:
|
|
763
|
+
"""
|
|
764
|
+
Write a UTF-8 text file into the container at `dirpath/filename` using
|
|
765
|
+
Docker's put_archive API. Cleaner than `exec cat > file` — no shell
|
|
766
|
+
escaping, no interference from shell initialisation.
|
|
767
|
+
"""
|
|
768
|
+
import io, tarfile, time as _t
|
|
769
|
+
data = text.encode("utf-8")
|
|
770
|
+
buf = io.BytesIO()
|
|
771
|
+
with tarfile.open(fileobj=buf, mode="w") as tar:
|
|
772
|
+
info = tarfile.TarInfo(name=filename)
|
|
773
|
+
info.size = len(data)
|
|
774
|
+
info.mtime = int(_t.time())
|
|
775
|
+
info.mode = 0o644
|
|
776
|
+
tar.addfile(info, io.BytesIO(data))
|
|
777
|
+
buf.seek(0)
|
|
778
|
+
# put_archive returns False on extraction failure (e.g. dirpath does not
|
|
779
|
+
# exist) and True on success. Without this check the caller logs success
|
|
780
|
+
# even though the file was never written. APIError still propagates as
|
|
781
|
+
# an exception per docker-py docs.
|
|
782
|
+
if not container.put_archive(dirpath, buf.getvalue()):
|
|
783
|
+
raise RuntimeError(
|
|
784
|
+
f"put_archive returned False writing {dirpath}/{filename} "
|
|
785
|
+
f"to container {container.short_id} — target dir may not exist"
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _get_container_user_and_workdir() -> tuple:
|
|
790
|
+
"""Get user and workdir based on Docker image type."""
|
|
791
|
+
use_entrypoint = "computer-use" in DOCKER_IMAGE or "open-computer-use" in DOCKER_IMAGE
|
|
792
|
+
if use_entrypoint:
|
|
793
|
+
return "assistant", "/home/assistant"
|
|
794
|
+
else:
|
|
795
|
+
return None, "/root" # None = use container default
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _reset_shutdown_timer(container, timeout: int = None):
|
|
799
|
+
"""Reset container auto-shutdown timer.
|
|
800
|
+
|
|
801
|
+
Args:
|
|
802
|
+
container: Docker container instance
|
|
803
|
+
timeout: Custom timeout in seconds. If None, uses CONTAINER_IDLE_TIMEOUT.
|
|
804
|
+
Used by long-running commands (e.g. sub_agent) to prevent
|
|
805
|
+
the idle timer from killing the container mid-execution.
|
|
806
|
+
"""
|
|
807
|
+
user, _ = _get_container_user_and_workdir()
|
|
808
|
+
exec_kwargs = {} if user is None else {"user": user}
|
|
809
|
+
|
|
810
|
+
effective_timeout = timeout if timeout else CONTAINER_IDLE_TIMEOUT
|
|
811
|
+
|
|
812
|
+
# Atomic timer reset: flock serializes concurrent resets so only one timer exists.
|
|
813
|
+
# The outer bash PID (MYPID=$$) is tracked in the file.
|
|
814
|
+
# When a new reset arrives: kill children (sleep) FIRST, then the bash parent.
|
|
815
|
+
# Order matters: killing bash first reparents sleep to PID 1, making pkill -P miss it.
|
|
816
|
+
# flock is released before sleep starts, so it doesn't block future resets.
|
|
817
|
+
timer_cmd = (
|
|
818
|
+
f"bash -c '"
|
|
819
|
+
f"MYPID=$$; "
|
|
820
|
+
f"(flock -x 9; "
|
|
821
|
+
f"OLD=$(cat /tmp/.shutdown-timer-pid 2>/dev/null); "
|
|
822
|
+
f'[ -n "$OLD" ] && pkill -P "$OLD" 2>/dev/null; '
|
|
823
|
+
f'[ -n "$OLD" ] && kill "$OLD" 2>/dev/null; '
|
|
824
|
+
f"echo $MYPID > /tmp/.shutdown-timer-pid"
|
|
825
|
+
f") 9>/tmp/.shutdown-timer-lock; "
|
|
826
|
+
f"sleep {effective_timeout} && kill 1"
|
|
827
|
+
f"'"
|
|
828
|
+
)
|
|
829
|
+
container.exec_run(timer_cmd, detach=True, **exec_kwargs)
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _execute_bash(container, command: str, timeout: int = None) -> dict:
|
|
833
|
+
"""Execute bash command in container with timeout."""
|
|
834
|
+
user, workdir = _get_container_user_and_workdir()
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
cmd_timeout = timeout if timeout is not None else COMMAND_TIMEOUT
|
|
838
|
+
# Ensure shutdown timer won't kill container before command finishes
|
|
839
|
+
shutdown_timeout = max(CONTAINER_IDLE_TIMEOUT, cmd_timeout + 60)
|
|
840
|
+
_reset_shutdown_timer(container, shutdown_timeout)
|
|
841
|
+
timed_command = f"timeout {cmd_timeout} bash -c {shlex.quote(command)}"
|
|
842
|
+
|
|
843
|
+
exec_result = container.exec_run(
|
|
844
|
+
cmd=["bash", "-c", timed_command],
|
|
845
|
+
stdout=True,
|
|
846
|
+
stderr=True,
|
|
847
|
+
demux=True,
|
|
848
|
+
workdir=workdir
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
stdout_data, stderr_data = exec_result.output if exec_result.output else (b"", b"")
|
|
852
|
+
stdout = stdout_data.decode("utf-8", errors="replace") if stdout_data else ""
|
|
853
|
+
stderr = stderr_data.decode("utf-8", errors="replace") if stderr_data else ""
|
|
854
|
+
|
|
855
|
+
output = ""
|
|
856
|
+
if stdout:
|
|
857
|
+
output += stdout
|
|
858
|
+
if stderr:
|
|
859
|
+
if output:
|
|
860
|
+
output += "\n"
|
|
861
|
+
output += stderr
|
|
862
|
+
|
|
863
|
+
if exec_result.exit_code == 124:
|
|
864
|
+
output += f"\n[Command timed out after {cmd_timeout} seconds]"
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
"exit_code": exec_result.exit_code,
|
|
868
|
+
"output": output,
|
|
869
|
+
"success": exec_result.exit_code == 0
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
except Exception as e:
|
|
873
|
+
return {
|
|
874
|
+
"exit_code": -1,
|
|
875
|
+
"output": f"Execution error: {str(e)}",
|
|
876
|
+
"success": False
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
# ---------------------------------------------------------------------------
|
|
881
|
+
# ADAPT-05 / Phase 5: capture variant of _execute_bash.
|
|
882
|
+
#
|
|
883
|
+
# _execute_bash returns {output, exit_code, success} where stdout+stderr are
|
|
884
|
+
# concatenated. Adapter parse_result(stdout, stderr, returncode) needs them
|
|
885
|
+
# separated, so cli_runtime.dispatch uses this helper instead. Same docker
|
|
886
|
+
# exec semantics (timeout, shutdown-timer reset, demux=True), different
|
|
887
|
+
# return shape.
|
|
888
|
+
#
|
|
889
|
+
# Returns a SimpleNamespace so callers can do `.stdout`, `.stderr`,
|
|
890
|
+
# `.returncode` (matches subprocess.CompletedProcess shape — adapter parsers
|
|
891
|
+
# are written against that idiom). SimpleNamespace is imported at the top of
|
|
892
|
+
# the module (PEP 8 — do NOT inline the import here).
|
|
893
|
+
# ---------------------------------------------------------------------------
|
|
894
|
+
def _execute_bash_capture(container, command: str, timeout: int = None):
|
|
895
|
+
"""Execute bash in container; return SimpleNamespace(stdout, stderr, returncode).
|
|
896
|
+
|
|
897
|
+
Stdout/stderr are kept separate (unlike _execute_bash which concatenates).
|
|
898
|
+
Used by cli_runtime.dispatch to feed adapter.parse_result, which is
|
|
899
|
+
written against the subprocess.CompletedProcess (stdout, stderr,
|
|
900
|
+
returncode) shape.
|
|
901
|
+
|
|
902
|
+
SECURITY (Phase 5 threat model T-05-05-01): the `command` argument is
|
|
903
|
+
passed straight to bash -c via shlex.quote — caller is responsible for
|
|
904
|
+
having shlex.quote'd every shell-significant value. cli_runtime.dispatch
|
|
905
|
+
constructs the command from `shlex.quote`'d argv elements; do not call
|
|
906
|
+
this helper with operator-controlled raw strings.
|
|
907
|
+
"""
|
|
908
|
+
user, workdir = _get_container_user_and_workdir()
|
|
909
|
+
try:
|
|
910
|
+
cmd_timeout = timeout if timeout is not None else COMMAND_TIMEOUT
|
|
911
|
+
shutdown_timeout = max(CONTAINER_IDLE_TIMEOUT, cmd_timeout + 60)
|
|
912
|
+
_reset_shutdown_timer(container, shutdown_timeout)
|
|
913
|
+
timed_command = f"timeout {cmd_timeout} bash -c {shlex.quote(command)}"
|
|
914
|
+
|
|
915
|
+
exec_result = container.exec_run(
|
|
916
|
+
cmd=["bash", "-c", timed_command],
|
|
917
|
+
stdout=True,
|
|
918
|
+
stderr=True,
|
|
919
|
+
demux=True,
|
|
920
|
+
workdir=workdir,
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
stdout_data, stderr_data = exec_result.output if exec_result.output else (b"", b"")
|
|
924
|
+
stdout = stdout_data.decode("utf-8", errors="replace") if stdout_data else ""
|
|
925
|
+
stderr = stderr_data.decode("utf-8", errors="replace") if stderr_data else ""
|
|
926
|
+
|
|
927
|
+
return SimpleNamespace(
|
|
928
|
+
stdout=stdout,
|
|
929
|
+
stderr=stderr,
|
|
930
|
+
returncode=exec_result.exit_code,
|
|
931
|
+
)
|
|
932
|
+
except Exception as e:
|
|
933
|
+
return SimpleNamespace(
|
|
934
|
+
stdout="",
|
|
935
|
+
stderr=f"Execution error: {str(e)}",
|
|
936
|
+
returncode=-1,
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def execute_bash_streaming(container, command: str, timeout: int, on_output_line=None) -> dict:
|
|
941
|
+
"""Execute bash in container with streaming output.
|
|
942
|
+
|
|
943
|
+
Calls on_output_line(line) for each non-empty output line as it arrives.
|
|
944
|
+
Returns dict with output (full text), exit_code, success.
|
|
945
|
+
"""
|
|
946
|
+
user, workdir = _get_container_user_and_workdir()
|
|
947
|
+
try:
|
|
948
|
+
cmd_timeout = timeout - 5 if timeout > 10 else timeout
|
|
949
|
+
shutdown_timeout = max(CONTAINER_IDLE_TIMEOUT, cmd_timeout + 60)
|
|
950
|
+
_reset_shutdown_timer(container, shutdown_timeout)
|
|
951
|
+
|
|
952
|
+
timed_command = f"timeout {cmd_timeout} bash -c {shlex.quote(command)}"
|
|
953
|
+
client = container.client
|
|
954
|
+
|
|
955
|
+
exec_id = client.api.exec_create(
|
|
956
|
+
container.id,
|
|
957
|
+
["bash", "-c", timed_command],
|
|
958
|
+
stdout=True,
|
|
959
|
+
stderr=True,
|
|
960
|
+
workdir=workdir,
|
|
961
|
+
)["Id"]
|
|
962
|
+
|
|
963
|
+
chunks = []
|
|
964
|
+
remainder = ""
|
|
965
|
+
for chunk in client.api.exec_start(exec_id, stream=True):
|
|
966
|
+
decoded = chunk.decode("utf-8", errors="replace")
|
|
967
|
+
chunks.append(decoded)
|
|
968
|
+
if on_output_line:
|
|
969
|
+
lines = (remainder + decoded).split("\n")
|
|
970
|
+
remainder = lines[-1]
|
|
971
|
+
for line in lines[:-1]:
|
|
972
|
+
stripped = line.strip()
|
|
973
|
+
if stripped:
|
|
974
|
+
on_output_line(stripped[:120])
|
|
975
|
+
|
|
976
|
+
if on_output_line and remainder.strip():
|
|
977
|
+
on_output_line(remainder.strip()[:120])
|
|
978
|
+
|
|
979
|
+
output = "".join(chunks)
|
|
980
|
+
info = client.api.exec_inspect(exec_id)
|
|
981
|
+
exit_code = info.get("ExitCode") or 0
|
|
982
|
+
|
|
983
|
+
if exit_code == 124:
|
|
984
|
+
output += f"\n[Command timed out after {cmd_timeout} seconds]"
|
|
985
|
+
|
|
986
|
+
return {"output": output, "exit_code": exit_code, "success": exit_code == 0}
|
|
987
|
+
|
|
988
|
+
except Exception as e:
|
|
989
|
+
return {"exit_code": -1, "output": f"Execution error: {str(e)}", "success": False}
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _get_meta_path(chat_id: str) -> Path:
|
|
993
|
+
"""Path to .meta.json for this chat on the host filesystem."""
|
|
994
|
+
from security import sanitize_chat_id
|
|
995
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
996
|
+
return BASE_DATA_DIR / chat_id / ".meta.json"
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def save_container_meta(chat_id: str, user_email: str, user_name: str,
|
|
1000
|
+
mcp_servers: str):
|
|
1001
|
+
"""Persist non-secret metadata needed to recreate a container after removal.
|
|
1002
|
+
|
|
1003
|
+
NO secrets/tokens stored — they come from computer-use-orchestrator ENV at resurrect time.
|
|
1004
|
+
"""
|
|
1005
|
+
meta = {
|
|
1006
|
+
"user_email": user_email or "",
|
|
1007
|
+
"user_name": user_name or "",
|
|
1008
|
+
"mcp_servers": mcp_servers or "",
|
|
1009
|
+
"created_at": datetime.datetime.utcnow().isoformat() + "Z",
|
|
1010
|
+
}
|
|
1011
|
+
meta_path = _get_meta_path(chat_id)
|
|
1012
|
+
try:
|
|
1013
|
+
meta_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1014
|
+
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2))
|
|
1015
|
+
print(f"[META] Saved metadata: {meta_path}")
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
print(f"[META] Warning: failed to save metadata: {e}")
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def load_container_meta(chat_id: str) -> Optional[dict]:
|
|
1021
|
+
"""Load saved metadata for container recreation. Returns dict or None."""
|
|
1022
|
+
meta_path = _get_meta_path(chat_id)
|
|
1023
|
+
try:
|
|
1024
|
+
if meta_path.exists():
|
|
1025
|
+
return json.loads(meta_path.read_text())
|
|
1026
|
+
except Exception as e:
|
|
1027
|
+
print(f"[META] Warning: failed to load metadata: {e}")
|
|
1028
|
+
return None
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _execute_python_with_stdin(container, script: str, data: str) -> dict:
|
|
1032
|
+
"""Execute Python script in container with data passed through stdin."""
|
|
1033
|
+
import socket as sock_module
|
|
1034
|
+
|
|
1035
|
+
_reset_shutdown_timer(container)
|
|
1036
|
+
user, workdir = _get_container_user_and_workdir()
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
exec_create_kwargs = {
|
|
1040
|
+
"stdin": True,
|
|
1041
|
+
"stdout": True,
|
|
1042
|
+
"stderr": True,
|
|
1043
|
+
"workdir": workdir,
|
|
1044
|
+
}
|
|
1045
|
+
if user:
|
|
1046
|
+
exec_create_kwargs["user"] = user
|
|
1047
|
+
|
|
1048
|
+
exec_id = container.client.api.exec_create(
|
|
1049
|
+
container.id,
|
|
1050
|
+
["timeout", str(COMMAND_TIMEOUT), "python3", "-c", script],
|
|
1051
|
+
**exec_create_kwargs
|
|
1052
|
+
)['Id']
|
|
1053
|
+
|
|
1054
|
+
sock = container.client.api.exec_start(exec_id, socket=True)
|
|
1055
|
+
|
|
1056
|
+
data_bytes = data.encode('utf-8')
|
|
1057
|
+
|
|
1058
|
+
if hasattr(sock, '_sock'):
|
|
1059
|
+
sock._sock.sendall(data_bytes)
|
|
1060
|
+
sock._sock.shutdown(sock_module.SHUT_WR)
|
|
1061
|
+
else:
|
|
1062
|
+
sock.sendall(data_bytes)
|
|
1063
|
+
if hasattr(sock, 'shutdown_write'):
|
|
1064
|
+
sock.shutdown_write()
|
|
1065
|
+
|
|
1066
|
+
gen = frames_iter(sock, tty=False)
|
|
1067
|
+
gen = (demux_adaptor(*frame) for frame in gen)
|
|
1068
|
+
stdout_data, stderr_data = consume_socket_output(gen, demux=True)
|
|
1069
|
+
|
|
1070
|
+
sock.close()
|
|
1071
|
+
|
|
1072
|
+
exec_info = container.client.api.exec_inspect(exec_id)
|
|
1073
|
+
exit_code = exec_info['ExitCode']
|
|
1074
|
+
|
|
1075
|
+
stdout = stdout_data.decode("utf-8", errors="replace") if stdout_data else ""
|
|
1076
|
+
stderr = stderr_data.decode("utf-8", errors="replace") if stderr_data else ""
|
|
1077
|
+
|
|
1078
|
+
output = ""
|
|
1079
|
+
if stdout:
|
|
1080
|
+
output += stdout
|
|
1081
|
+
if stderr:
|
|
1082
|
+
if output:
|
|
1083
|
+
output += "\n"
|
|
1084
|
+
output += stderr
|
|
1085
|
+
|
|
1086
|
+
if exit_code == 124:
|
|
1087
|
+
output += f"\n[Command timed out after {COMMAND_TIMEOUT} seconds]"
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
"exit_code": exit_code,
|
|
1091
|
+
"output": output,
|
|
1092
|
+
"success": exit_code == 0
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
except Exception as e:
|
|
1096
|
+
return {
|
|
1097
|
+
"exit_code": -1,
|
|
1098
|
+
"output": f"Execution error: {str(e)}",
|
|
1099
|
+
"success": False
|
|
1100
|
+
}
|