@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,1544 @@
|
|
|
1
|
+
# SPDX-License-Identifier: FSL-1.1-Apache-2.0
|
|
2
|
+
# Copyright (c) 2025 Open Computer Use Contributors
|
|
3
|
+
"""
|
|
4
|
+
File Server for Computer Use Outputs + MCP Endpoint
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
1. HTTP API for file upload/download
|
|
8
|
+
2. MCP (Model Context Protocol) endpoint for Computer Use tools
|
|
9
|
+
|
|
10
|
+
See /docs for Swagger UI, /redoc for ReDoc, / for HTML documentation.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import asyncio
|
|
15
|
+
import html as html_module
|
|
16
|
+
import hashlib
|
|
17
|
+
import json
|
|
18
|
+
import mimetypes
|
|
19
|
+
import time
|
|
20
|
+
import zipfile
|
|
21
|
+
from io import BytesIO
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Optional, Dict, List, Any
|
|
24
|
+
|
|
25
|
+
import aiohttp
|
|
26
|
+
from fastapi import FastAPI, HTTPException, Header, UploadFile, File, Request, Response, Depends, WebSocket, WebSocketDisconnect, Body
|
|
27
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
28
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
29
|
+
from fastapi.responses import FileResponse, StreamingResponse, HTMLResponse, PlainTextResponse
|
|
30
|
+
from fastapi.staticfiles import StaticFiles
|
|
31
|
+
from pydantic import BaseModel, Field
|
|
32
|
+
|
|
33
|
+
from system_prompt import SYSTEM_PROMPT_TEMPLATE, build_system_prompt, render_system_prompt
|
|
34
|
+
from docker_manager import (
|
|
35
|
+
get_container_cdp_address,
|
|
36
|
+
PUBLIC_BASE_URL,
|
|
37
|
+
warn_if_public_base_url_is_default,
|
|
38
|
+
warn_if_mcp_api_key_missing,
|
|
39
|
+
warn_subagent_cli,
|
|
40
|
+
)
|
|
41
|
+
from security import sanitize_chat_id, safe_path
|
|
42
|
+
import skill_manager
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# =============================================================================
|
|
46
|
+
# MCP Authorization
|
|
47
|
+
# =============================================================================
|
|
48
|
+
|
|
49
|
+
MCP_API_KEY = os.getenv("MCP_API_KEY") # Required for /mcp endpoints
|
|
50
|
+
|
|
51
|
+
security = HTTPBearer(auto_error=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def verify_mcp_auth(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
|
55
|
+
"""Verify Bearer token for MCP endpoints."""
|
|
56
|
+
if not MCP_API_KEY:
|
|
57
|
+
# If no key configured, allow all requests (development mode)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if not credentials:
|
|
61
|
+
raise HTTPException(
|
|
62
|
+
status_code=401,
|
|
63
|
+
detail="Missing Authorization header",
|
|
64
|
+
headers={"WWW-Authenticate": "Bearer"}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if credentials.credentials != MCP_API_KEY:
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=401,
|
|
70
|
+
detail="Invalid API key",
|
|
71
|
+
headers={"WWW-Authenticate": "Bearer"}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return credentials.credentials
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# Pydantic Models for Swagger Documentation
|
|
79
|
+
# =============================================================================
|
|
80
|
+
|
|
81
|
+
class MCPRequest(BaseModel):
|
|
82
|
+
"""MCP JSON-RPC Request"""
|
|
83
|
+
jsonrpc: str = Field(default="2.0", description="JSON-RPC version")
|
|
84
|
+
id: int = Field(..., description="Request ID")
|
|
85
|
+
method: str = Field(..., description="MCP method: initialize, tools/list, tools/call")
|
|
86
|
+
params: Dict[str, Any] = Field(default={}, description="Method parameters")
|
|
87
|
+
|
|
88
|
+
model_config = {
|
|
89
|
+
"json_schema_extra": {
|
|
90
|
+
"examples": [
|
|
91
|
+
{
|
|
92
|
+
"jsonrpc": "2.0",
|
|
93
|
+
"id": 1,
|
|
94
|
+
"method": "initialize",
|
|
95
|
+
"params": {
|
|
96
|
+
"protocolVersion": "2024-11-05",
|
|
97
|
+
"capabilities": {},
|
|
98
|
+
"clientInfo": {"name": "test", "version": "1.0"}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"jsonrpc": "2.0",
|
|
103
|
+
"id": 2,
|
|
104
|
+
"method": "tools/call",
|
|
105
|
+
"params": {
|
|
106
|
+
"name": "bash_tool",
|
|
107
|
+
"arguments": {"command": "echo hello", "description": "test"}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class MCPResponse(BaseModel):
|
|
116
|
+
"""MCP JSON-RPC Response"""
|
|
117
|
+
jsonrpc: str = "2.0"
|
|
118
|
+
id: Optional[int] = None
|
|
119
|
+
result: Optional[Dict[str, Any]] = None
|
|
120
|
+
error: Optional[Dict[str, Any]] = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MCPToolInfo(BaseModel):
|
|
124
|
+
"""MCP Tool information"""
|
|
125
|
+
name: str
|
|
126
|
+
description: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MCPInfo(BaseModel):
|
|
130
|
+
"""MCP Server information"""
|
|
131
|
+
name: str
|
|
132
|
+
version: str
|
|
133
|
+
description: str
|
|
134
|
+
tools: List[MCPToolInfo]
|
|
135
|
+
headers: Dict[str, List[str]]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class UploadResponse(BaseModel):
|
|
139
|
+
"""File upload response"""
|
|
140
|
+
status: str
|
|
141
|
+
filename: str
|
|
142
|
+
size: int
|
|
143
|
+
md5: str
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# =============================================================================
|
|
147
|
+
# FastAPI Application
|
|
148
|
+
# =============================================================================
|
|
149
|
+
|
|
150
|
+
SWAGGER_DESCRIPTION = """
|
|
151
|
+
## Computer Use Server + MCP
|
|
152
|
+
|
|
153
|
+
HTTP API for file upload/download and **MCP (Model Context Protocol)** endpoint for Computer Use tools.
|
|
154
|
+
|
|
155
|
+
### Quick Start
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Step 1: Initialize (get session ID)
|
|
159
|
+
curl -sD - -X POST "http://localhost:8081/mcp" \\
|
|
160
|
+
-H "Authorization: Bearer <MCP_API_KEY>" \\
|
|
161
|
+
-H "Content-Type: application/json" \\
|
|
162
|
+
-H "Accept: application/json, text/event-stream" \\
|
|
163
|
+
-H "X-Chat-Id: my-session" \\
|
|
164
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
|
|
165
|
+
# Response contains header: mcp-session-id: <SESSION_ID>
|
|
166
|
+
|
|
167
|
+
# Step 2: Call a tool
|
|
168
|
+
curl -s -X POST "http://localhost:8081/mcp" \\
|
|
169
|
+
-H "Authorization: Bearer <MCP_API_KEY>" \\
|
|
170
|
+
-H "Content-Type: application/json" \\
|
|
171
|
+
-H "Accept: application/json, text/event-stream" \\
|
|
172
|
+
-H "Mcp-Session-Id: <SESSION_ID>" \\
|
|
173
|
+
-H "X-Chat-Id: my-session" \\
|
|
174
|
+
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"bash_tool","arguments":{"command":"echo Hello","description":"test"}}}'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### MCP Tools
|
|
178
|
+
|
|
179
|
+
- **bash_tool** - execute bash commands in isolated Docker container
|
|
180
|
+
- **view** - view files and directories
|
|
181
|
+
- **create_file** - create new files
|
|
182
|
+
- **str_replace** - edit files (text replacement)
|
|
183
|
+
- **sub_agent** - delegate tasks to autonomous agent (Claude Code)
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
from contextlib import asynccontextmanager
|
|
187
|
+
|
|
188
|
+
@asynccontextmanager
|
|
189
|
+
async def lifespan(app):
|
|
190
|
+
"""FastAPI lifespan: start MCP session manager for Streamable HTTP.
|
|
191
|
+
|
|
192
|
+
Do NOT swallow ImportError here. The previous version did, and a missing
|
|
193
|
+
`mcp_resources` (e.g. not COPYed in Dockerfile) caused the lifespan to
|
|
194
|
+
yield WITHOUT calling `session_manager.run()`. The MCP ASGI app then
|
|
195
|
+
served every /mcp request with HTTP 500 ("Task group is not
|
|
196
|
+
initialized"), and uvicorn's default error handler hid the traceback —
|
|
197
|
+
a 100% silent failure mode that took hours of bisecting to find.
|
|
198
|
+
|
|
199
|
+
If imports fail, crash loudly so the deploy is obviously broken.
|
|
200
|
+
"""
|
|
201
|
+
warn_if_public_base_url_is_default()
|
|
202
|
+
warn_if_mcp_api_key_missing()
|
|
203
|
+
warn_subagent_cli()
|
|
204
|
+
from mcp_tools import mcp as _mcp_server
|
|
205
|
+
# Import-for-side-effect: registers @mcp.resource handlers on the
|
|
206
|
+
# FastMCP singleton. Must happen BEFORE streamable_http_app() so the
|
|
207
|
+
# resources capability is advertised in InitializeResult.
|
|
208
|
+
import mcp_resources # noqa: F401
|
|
209
|
+
if _mcp_server._session_manager is None:
|
|
210
|
+
_mcp_server.streamable_http_app() # triggers lazy init of session_manager
|
|
211
|
+
async with _mcp_server.session_manager.run():
|
|
212
|
+
print("[MCP] session_manager.run() entered — /mcp endpoint is live")
|
|
213
|
+
yield
|
|
214
|
+
print("[MCP] session_manager.run() exiting")
|
|
215
|
+
|
|
216
|
+
app = FastAPI(
|
|
217
|
+
title="Computer Use File Server + MCP",
|
|
218
|
+
description=SWAGGER_DESCRIPTION,
|
|
219
|
+
version="2.0.0",
|
|
220
|
+
docs_url="/docs",
|
|
221
|
+
redoc_url="/redoc",
|
|
222
|
+
lifespan=lifespan,
|
|
223
|
+
openapi_tags=[
|
|
224
|
+
{"name": "MCP", "description": "Model Context Protocol endpoint for Computer Use tools"},
|
|
225
|
+
{"name": "Files", "description": "File upload and download"},
|
|
226
|
+
{"name": "System", "description": "Health check and service information"},
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# CORS — needed for preview SPA loaded inside iframe (opaque origin → cross-origin fetch)
|
|
231
|
+
app.add_middleware(
|
|
232
|
+
CORSMiddleware,
|
|
233
|
+
allow_origins=["*"],
|
|
234
|
+
allow_methods=["GET", "POST", "OPTIONS"],
|
|
235
|
+
allow_headers=["*"],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Normalize chat_id in URL paths to lowercase.
|
|
239
|
+
# Docker container names are case-sensitive, but browser URLs may contain
|
|
240
|
+
# uppercase hex in UUIDs (e.g. 384B vs 384b). This prevents 404s.
|
|
241
|
+
@app.middleware("http")
|
|
242
|
+
async def normalize_chat_id_case(request, call_next):
|
|
243
|
+
import re as _re
|
|
244
|
+
path = request.scope.get("path", "")
|
|
245
|
+
# Match UUID-like segments in paths like /files/{chat_id}/... or /terminal/{chat_id}/...
|
|
246
|
+
normalized = _re.sub(
|
|
247
|
+
r'/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})',
|
|
248
|
+
lambda m: '/' + m.group(1).lower(),
|
|
249
|
+
path
|
|
250
|
+
)
|
|
251
|
+
if normalized != path:
|
|
252
|
+
request.scope["path"] = normalized
|
|
253
|
+
return await call_next(request)
|
|
254
|
+
|
|
255
|
+
# Static files (bundled JS/CSS libraries)
|
|
256
|
+
_static_dir = Path(__file__).parent / "static"
|
|
257
|
+
if _static_dir.is_dir():
|
|
258
|
+
app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
|
|
259
|
+
|
|
260
|
+
# Base directory where chat data is stored
|
|
261
|
+
# Mounted from host: /tmp/computer-use-data/{chat_id}/outputs/
|
|
262
|
+
BASE_DATA_DIR = Path(os.getenv("BASE_DATA_DIR", "/data"))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# =============================================================================
|
|
266
|
+
# File Classification for Preview
|
|
267
|
+
# =============================================================================
|
|
268
|
+
|
|
269
|
+
_CODE_EXTENSIONS = {
|
|
270
|
+
'.py', '.js', '.ts', '.jsx', '.tsx', '.css', '.json', '.yaml', '.yml',
|
|
271
|
+
'.toml', '.sh', '.bash', '.sql', '.go', '.rs', '.java', '.c', '.cpp',
|
|
272
|
+
'.h', '.hpp', '.rb', '.php', '.swift', '.kt', '.scala', '.r', '.m',
|
|
273
|
+
'.vue', '.svelte', '.xml', '.dockerfile',
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_TEXT_EXTENSIONS = {'.txt', '.log', '.ini', '.cfg', '.conf', '.env'}
|
|
277
|
+
|
|
278
|
+
_MARKDOWN_EXTENSIONS = {'.md', '.markdown'}
|
|
279
|
+
|
|
280
|
+
_SPREADSHEET_EXTENSIONS = {'.csv', '.tsv'}
|
|
281
|
+
|
|
282
|
+
_IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico'}
|
|
283
|
+
|
|
284
|
+
_AUDIO_EXTENSIONS = {'.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac', '.wma'}
|
|
285
|
+
_VIDEO_EXTENSIONS = {'.mp4', '.webm', '.mkv', '.mov', '.avi'}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def classify_file(filename: str) -> tuple:
|
|
289
|
+
"""Classify file by extension. Returns (type_category, mime_type)."""
|
|
290
|
+
ext = Path(filename).suffix.lower()
|
|
291
|
+
mime, _ = mimetypes.guess_type(filename)
|
|
292
|
+
if mime is None:
|
|
293
|
+
mime = 'application/octet-stream'
|
|
294
|
+
|
|
295
|
+
if ext in ('.html', '.htm'):
|
|
296
|
+
return 'html', mime
|
|
297
|
+
if ext in _IMAGE_EXTENSIONS:
|
|
298
|
+
return 'image', mime
|
|
299
|
+
if ext in _AUDIO_EXTENSIONS:
|
|
300
|
+
return 'audio', mime or 'audio/mpeg'
|
|
301
|
+
if ext in _VIDEO_EXTENSIONS:
|
|
302
|
+
return 'video', mime or 'video/mp4'
|
|
303
|
+
if ext == '.pdf':
|
|
304
|
+
return 'pdf', 'application/pdf'
|
|
305
|
+
if ext in _MARKDOWN_EXTENSIONS:
|
|
306
|
+
return 'markdown', 'text/markdown'
|
|
307
|
+
if ext in _SPREADSHEET_EXTENSIONS:
|
|
308
|
+
return 'spreadsheet', mime
|
|
309
|
+
if ext == '.docx':
|
|
310
|
+
return 'docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
311
|
+
if ext in ('.xlsx', '.xls'):
|
|
312
|
+
return 'xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
313
|
+
if ext == '.pptx':
|
|
314
|
+
return 'pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
315
|
+
if ext == '.drawio':
|
|
316
|
+
return 'drawio', 'application/xml'
|
|
317
|
+
if ext in _CODE_EXTENSIONS:
|
|
318
|
+
return 'code', mime
|
|
319
|
+
if ext in _TEXT_EXTENSIONS:
|
|
320
|
+
return 'text', mime
|
|
321
|
+
# Fallback: check mime type
|
|
322
|
+
if mime.startswith('image/'):
|
|
323
|
+
return 'image', mime
|
|
324
|
+
if mime.startswith('audio/'):
|
|
325
|
+
return 'audio', mime
|
|
326
|
+
if mime.startswith('video/'):
|
|
327
|
+
return 'video', mime
|
|
328
|
+
if mime.startswith('text/'):
|
|
329
|
+
return 'text', mime
|
|
330
|
+
return 'other', mime
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def format_size(size_bytes: int) -> str:
|
|
334
|
+
"""Format file size for display."""
|
|
335
|
+
if size_bytes < 1024:
|
|
336
|
+
return f"{size_bytes} B"
|
|
337
|
+
if size_bytes < 1024 * 1024:
|
|
338
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
339
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
_FILE_ICONS = {
|
|
343
|
+
'html': '🌐', 'image': '🖼️', 'code': '📝', 'text': '📄',
|
|
344
|
+
'pdf': '📕', 'markdown': '📃', 'spreadsheet': '📊',
|
|
345
|
+
'docx': '📄', 'xlsx': '📊', 'pptx': '📊',
|
|
346
|
+
'audio': '🎵', 'video': '🎬', 'other': '📎',
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@app.get("/", response_class=HTMLResponse, tags=["System"], include_in_schema=False)
|
|
352
|
+
async def root():
|
|
353
|
+
"""Main page with MCP and File API documentation"""
|
|
354
|
+
from docs_html import get_root_html
|
|
355
|
+
return get_root_html()
|
|
356
|
+
|
|
357
|
+
@app.get("/api/uploads/{chat_id}/manifest", tags=["Files"])
|
|
358
|
+
async def get_uploads_manifest(chat_id: str) -> Dict[str, str]:
|
|
359
|
+
"""
|
|
360
|
+
Get manifest of uploaded files with their MD5 checksums.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
chat_id: Unique chat identifier
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Dictionary mapping filename to MD5 checksum
|
|
367
|
+
Example: {"file1.txt": "abc123def456", "doc.pdf": "789xyz"}
|
|
368
|
+
"""
|
|
369
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
370
|
+
uploads_dir = safe_path(BASE_DATA_DIR, chat_id, "uploads")
|
|
371
|
+
|
|
372
|
+
# Return empty dict if directory doesn't exist yet
|
|
373
|
+
if not uploads_dir.exists():
|
|
374
|
+
return {}
|
|
375
|
+
|
|
376
|
+
manifest = {}
|
|
377
|
+
|
|
378
|
+
# Scan all files in uploads directory
|
|
379
|
+
for file_path in uploads_dir.rglob("*"):
|
|
380
|
+
if file_path.is_file():
|
|
381
|
+
# Calculate MD5 checksum
|
|
382
|
+
md5_hash = hashlib.md5()
|
|
383
|
+
with open(file_path, "rb") as f:
|
|
384
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
385
|
+
md5_hash.update(chunk)
|
|
386
|
+
|
|
387
|
+
# Use relative filename as key
|
|
388
|
+
relative_name = file_path.relative_to(uploads_dir)
|
|
389
|
+
manifest[str(relative_name)] = md5_hash.hexdigest()
|
|
390
|
+
|
|
391
|
+
return manifest
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@app.get("/api/uploads/{chat_id}/list", tags=["Files"])
|
|
395
|
+
async def list_uploads(chat_id: str, response: Response):
|
|
396
|
+
"""List all files in the uploads directory with metadata."""
|
|
397
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
398
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
399
|
+
uploads_dir = safe_path(BASE_DATA_DIR, chat_id, "uploads")
|
|
400
|
+
if not uploads_dir.exists():
|
|
401
|
+
return {"files": [], "total": 0}
|
|
402
|
+
files = []
|
|
403
|
+
for fp in uploads_dir.rglob("*"):
|
|
404
|
+
if not fp.is_file():
|
|
405
|
+
continue
|
|
406
|
+
rel = fp.relative_to(uploads_dir)
|
|
407
|
+
stat = fp.stat()
|
|
408
|
+
files.append({
|
|
409
|
+
"name": fp.name,
|
|
410
|
+
"path": str(rel),
|
|
411
|
+
"size": stat.st_size,
|
|
412
|
+
"modified": stat.st_mtime,
|
|
413
|
+
"container_path": f"/mnt/user-data/uploads/{rel}",
|
|
414
|
+
})
|
|
415
|
+
files.sort(key=lambda f: f["modified"], reverse=True)
|
|
416
|
+
return {"files": files, "total": len(files)}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@app.post("/api/uploads/{chat_id}/{filename:path}", tags=["Files"], response_model=UploadResponse)
|
|
420
|
+
async def upload_file(chat_id: str, filename: str, file: UploadFile = File(...)):
|
|
421
|
+
"""
|
|
422
|
+
Upload a file to chat uploads directory.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
chat_id: Unique chat identifier
|
|
426
|
+
filename: Target filename (can include subdirectories)
|
|
427
|
+
file: File to upload
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Success message with file info
|
|
431
|
+
|
|
432
|
+
Raises:
|
|
433
|
+
400: Invalid filename or security violation
|
|
434
|
+
"""
|
|
435
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
436
|
+
# Create uploads directory if it doesn't exist
|
|
437
|
+
uploads_dir = safe_path(BASE_DATA_DIR, chat_id, "uploads")
|
|
438
|
+
uploads_dir.mkdir(parents=True, exist_ok=True)
|
|
439
|
+
|
|
440
|
+
# Construct target path with traversal protection
|
|
441
|
+
file_path = safe_path(uploads_dir, filename)
|
|
442
|
+
|
|
443
|
+
# Create parent directories if needed
|
|
444
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
445
|
+
|
|
446
|
+
# Save file
|
|
447
|
+
try:
|
|
448
|
+
with open(file_path, "wb") as f:
|
|
449
|
+
content = await file.read()
|
|
450
|
+
f.write(content)
|
|
451
|
+
|
|
452
|
+
# Calculate MD5 for confirmation
|
|
453
|
+
md5_hash = hashlib.md5(content).hexdigest()
|
|
454
|
+
|
|
455
|
+
# Tier 6 — refresh MCP resources for this chat so the new file
|
|
456
|
+
# appears in resources/list without reconnecting. Swallow errors
|
|
457
|
+
# so the upload itself succeeds even if MCP resource sync fails.
|
|
458
|
+
try:
|
|
459
|
+
from mcp_resources import sync_chat_resources
|
|
460
|
+
await sync_chat_resources(chat_id)
|
|
461
|
+
except Exception:
|
|
462
|
+
import traceback
|
|
463
|
+
print(f"[uploads] sync_chat_resources failed for chat {chat_id}:\n{traceback.format_exc()}")
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
"status": "success",
|
|
467
|
+
"filename": filename,
|
|
468
|
+
"size": len(content),
|
|
469
|
+
"md5": md5_hash
|
|
470
|
+
}
|
|
471
|
+
except Exception as e:
|
|
472
|
+
raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}")
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@app.get("/files/{chat_id}/archive", tags=["Files"])
|
|
476
|
+
async def download_archive(chat_id: str):
|
|
477
|
+
"""
|
|
478
|
+
Download entire outputs directory as a zip archive.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
chat_id: Unique chat identifier
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
StreamingResponse with zip archive
|
|
485
|
+
|
|
486
|
+
Raises:
|
|
487
|
+
404: Directory not found or empty
|
|
488
|
+
"""
|
|
489
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
490
|
+
# Construct outputs directory path
|
|
491
|
+
outputs_dir = safe_path(BASE_DATA_DIR, chat_id, "outputs")
|
|
492
|
+
|
|
493
|
+
# Check if directory exists
|
|
494
|
+
if not outputs_dir.exists():
|
|
495
|
+
raise HTTPException(
|
|
496
|
+
status_code=404,
|
|
497
|
+
detail=f"Outputs directory not found for chat: {chat_id}"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if not outputs_dir.is_dir():
|
|
501
|
+
raise HTTPException(
|
|
502
|
+
status_code=400,
|
|
503
|
+
detail="Path is not a directory"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Get all files in directory
|
|
507
|
+
files = list(outputs_dir.rglob("*"))
|
|
508
|
+
files = [f for f in files if f.is_file()]
|
|
509
|
+
|
|
510
|
+
if not files:
|
|
511
|
+
raise HTTPException(
|
|
512
|
+
status_code=404,
|
|
513
|
+
detail="No files found in outputs directory"
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# Create zip archive in memory
|
|
517
|
+
zip_buffer = BytesIO()
|
|
518
|
+
|
|
519
|
+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
520
|
+
for file_path in files:
|
|
521
|
+
# Add file to zip with relative path
|
|
522
|
+
arcname = file_path.relative_to(outputs_dir)
|
|
523
|
+
zip_file.write(file_path, arcname=str(arcname))
|
|
524
|
+
|
|
525
|
+
# Seek to beginning of buffer
|
|
526
|
+
zip_buffer.seek(0)
|
|
527
|
+
|
|
528
|
+
# Return as streaming response
|
|
529
|
+
return StreamingResponse(
|
|
530
|
+
zip_buffer,
|
|
531
|
+
media_type="application/zip",
|
|
532
|
+
headers={
|
|
533
|
+
"Content-Disposition": f"attachment; filename=chat-{chat_id}-outputs.zip"
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
@app.get("/files/{chat_id}/{filename:path}", tags=["Files"])
|
|
539
|
+
async def download_file(chat_id: str, filename: str, download: Optional[int] = None):
|
|
540
|
+
"""
|
|
541
|
+
Download a specific file from chat outputs directory.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
chat_id: Unique chat identifier
|
|
545
|
+
filename: File name (can include subdirectories)
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
FileResponse with the requested file
|
|
549
|
+
|
|
550
|
+
Raises:
|
|
551
|
+
404: File not found
|
|
552
|
+
"""
|
|
553
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
554
|
+
# Construct full path with traversal protection
|
|
555
|
+
outputs_dir = safe_path(BASE_DATA_DIR, chat_id, "outputs")
|
|
556
|
+
file_path = safe_path(outputs_dir, filename)
|
|
557
|
+
|
|
558
|
+
# Check if file exists
|
|
559
|
+
if not file_path.exists():
|
|
560
|
+
raise HTTPException(
|
|
561
|
+
status_code=404,
|
|
562
|
+
detail=f"File not found: {filename}"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
if not file_path.is_file():
|
|
566
|
+
raise HTTPException(
|
|
567
|
+
status_code=400,
|
|
568
|
+
detail=f"Path is not a file: {filename}"
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# Return file
|
|
572
|
+
# ?download=1 → force download (Content-Disposition: attachment)
|
|
573
|
+
# default → serve with real MIME type (browser displays inline)
|
|
574
|
+
if download:
|
|
575
|
+
return FileResponse(
|
|
576
|
+
path=file_path,
|
|
577
|
+
filename=file_path.name,
|
|
578
|
+
media_type="application/octet-stream"
|
|
579
|
+
)
|
|
580
|
+
else:
|
|
581
|
+
mime_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
|
|
582
|
+
return FileResponse(
|
|
583
|
+
path=file_path,
|
|
584
|
+
filename=file_path.name,
|
|
585
|
+
media_type=mime_type
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
# =============================================================================
|
|
590
|
+
# Output Files API + Preview
|
|
591
|
+
# =============================================================================
|
|
592
|
+
|
|
593
|
+
@app.get("/api/outputs/{chat_id}", tags=["Files"])
|
|
594
|
+
async def list_outputs(chat_id: str, response: Response):
|
|
595
|
+
"""List all files in the outputs directory with metadata."""
|
|
596
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
597
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
598
|
+
|
|
599
|
+
outputs_dir = safe_path(BASE_DATA_DIR, chat_id, "outputs")
|
|
600
|
+
|
|
601
|
+
if not outputs_dir.exists():
|
|
602
|
+
return {"chat_id": chat_id, "files": [], "total": 0, "timestamp": time.time()}
|
|
603
|
+
|
|
604
|
+
files = []
|
|
605
|
+
|
|
606
|
+
for file_path in outputs_dir.rglob("*"):
|
|
607
|
+
if not file_path.is_file():
|
|
608
|
+
continue
|
|
609
|
+
if file_path.name.startswith('.'):
|
|
610
|
+
continue
|
|
611
|
+
relative = file_path.relative_to(outputs_dir)
|
|
612
|
+
file_type, mime = classify_file(str(relative))
|
|
613
|
+
stat = file_path.stat()
|
|
614
|
+
files.append({
|
|
615
|
+
"name": file_path.name,
|
|
616
|
+
"path": str(relative),
|
|
617
|
+
"size": stat.st_size,
|
|
618
|
+
"modified": stat.st_mtime,
|
|
619
|
+
"type": file_type,
|
|
620
|
+
"mime": mime,
|
|
621
|
+
"url": f"/files/{chat_id}/{relative}",
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
files.sort(key=lambda f: f["modified"], reverse=True)
|
|
625
|
+
return {"chat_id": chat_id, "files": files, "total": len(files), "timestamp": time.time()}
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
# =============================================================================
|
|
629
|
+
# Browser CDP Proxy — live browser viewer
|
|
630
|
+
# =============================================================================
|
|
631
|
+
|
|
632
|
+
@app.get("/browser/{chat_id}/status", tags=["Browser"])
|
|
633
|
+
async def browser_status(chat_id: str, response: Response):
|
|
634
|
+
"""Check if browser (Chromium CDP) is running in the chat's container."""
|
|
635
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
636
|
+
response.headers["Cache-Control"] = "no-cache, no-store"
|
|
637
|
+
container_ip = get_container_cdp_address(chat_id)
|
|
638
|
+
if not container_ip:
|
|
639
|
+
return {"active": False, "pages": []}
|
|
640
|
+
|
|
641
|
+
try:
|
|
642
|
+
async with aiohttp.ClientSession() as session:
|
|
643
|
+
async with session.get(
|
|
644
|
+
f"http://{container_ip}:9222/json",
|
|
645
|
+
timeout=aiohttp.ClientTimeout(total=2)
|
|
646
|
+
) as resp:
|
|
647
|
+
pages = await resp.json()
|
|
648
|
+
return {
|
|
649
|
+
"active": True,
|
|
650
|
+
"pages": [
|
|
651
|
+
{"id": p.get("id", ""), "title": p.get("title", ""), "url": p.get("url", "")}
|
|
652
|
+
for p in pages
|
|
653
|
+
if p.get("type") == "page"
|
|
654
|
+
]
|
|
655
|
+
}
|
|
656
|
+
except Exception:
|
|
657
|
+
return {"active": False, "pages": []}
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@app.get("/browser/{chat_id}/json", tags=["Browser"])
|
|
661
|
+
async def browser_cdp_json(chat_id: str):
|
|
662
|
+
"""Proxy CDP /json endpoint from container."""
|
|
663
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
664
|
+
container_ip = get_container_cdp_address(chat_id)
|
|
665
|
+
if not container_ip:
|
|
666
|
+
raise HTTPException(404, "Container not found or not running")
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
async with aiohttp.ClientSession() as session:
|
|
670
|
+
async with session.get(
|
|
671
|
+
f"http://{container_ip}:9222/json",
|
|
672
|
+
timeout=aiohttp.ClientTimeout(total=3)
|
|
673
|
+
) as resp:
|
|
674
|
+
return await resp.json()
|
|
675
|
+
except Exception:
|
|
676
|
+
raise HTTPException(503, "Browser not available")
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@app.get("/browser/{chat_id}/json/version", tags=["Browser"])
|
|
680
|
+
async def browser_cdp_json_version(chat_id: str):
|
|
681
|
+
"""Proxy CDP /json/version endpoint from container."""
|
|
682
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
683
|
+
container_ip = get_container_cdp_address(chat_id)
|
|
684
|
+
if not container_ip:
|
|
685
|
+
raise HTTPException(404, "Container not found or not running")
|
|
686
|
+
|
|
687
|
+
try:
|
|
688
|
+
async with aiohttp.ClientSession() as session:
|
|
689
|
+
async with session.get(
|
|
690
|
+
f"http://{container_ip}:9222/json/version",
|
|
691
|
+
timeout=aiohttp.ClientTimeout(total=3)
|
|
692
|
+
) as resp:
|
|
693
|
+
return await resp.json()
|
|
694
|
+
except Exception:
|
|
695
|
+
raise HTTPException(503, "Browser not available")
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@app.websocket("/browser/{chat_id}/devtools/page/{page_id}")
|
|
699
|
+
async def browser_ws_proxy(websocket: WebSocket, chat_id: str, page_id: str):
|
|
700
|
+
"""Bidirectional WebSocket proxy for CDP — connects browser viewer to container's Chromium."""
|
|
701
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
702
|
+
container_ip = get_container_cdp_address(chat_id)
|
|
703
|
+
if not container_ip:
|
|
704
|
+
await websocket.close(code=1008, reason="Container not found")
|
|
705
|
+
return
|
|
706
|
+
|
|
707
|
+
await websocket.accept()
|
|
708
|
+
|
|
709
|
+
backend_url = f"ws://{container_ip}:9222/devtools/page/{page_id}"
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
async with aiohttp.ClientSession() as session:
|
|
713
|
+
async with session.ws_connect(backend_url) as backend_ws:
|
|
714
|
+
async def forward_client_to_backend():
|
|
715
|
+
try:
|
|
716
|
+
while True:
|
|
717
|
+
data = await websocket.receive_text()
|
|
718
|
+
await backend_ws.send_str(data)
|
|
719
|
+
except WebSocketDisconnect:
|
|
720
|
+
pass
|
|
721
|
+
except Exception:
|
|
722
|
+
pass
|
|
723
|
+
finally:
|
|
724
|
+
await backend_ws.close()
|
|
725
|
+
|
|
726
|
+
async def forward_backend_to_client():
|
|
727
|
+
try:
|
|
728
|
+
async for msg in backend_ws:
|
|
729
|
+
if msg.type == aiohttp.WSMsgType.TEXT:
|
|
730
|
+
await websocket.send_text(msg.data)
|
|
731
|
+
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
|
732
|
+
break
|
|
733
|
+
except Exception:
|
|
734
|
+
pass
|
|
735
|
+
finally:
|
|
736
|
+
try:
|
|
737
|
+
await websocket.close()
|
|
738
|
+
except Exception:
|
|
739
|
+
pass
|
|
740
|
+
|
|
741
|
+
done, pending = await asyncio.wait(
|
|
742
|
+
[
|
|
743
|
+
asyncio.create_task(forward_client_to_backend()),
|
|
744
|
+
asyncio.create_task(forward_backend_to_client()),
|
|
745
|
+
],
|
|
746
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
747
|
+
)
|
|
748
|
+
for task in pending:
|
|
749
|
+
task.cancel()
|
|
750
|
+
except Exception:
|
|
751
|
+
try:
|
|
752
|
+
await websocket.close(code=1011, reason="Backend connection failed")
|
|
753
|
+
except Exception:
|
|
754
|
+
pass
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
# =============================================================================
|
|
759
|
+
# Terminal Proxy — ttyd WebSocket terminal (like Browser CDP proxy)
|
|
760
|
+
# =============================================================================
|
|
761
|
+
|
|
762
|
+
def _get_container_for_terminal(chat_id: str):
|
|
763
|
+
"""Get running container for terminal access. Does NOT create containers."""
|
|
764
|
+
import re as _re
|
|
765
|
+
from docker_manager import get_docker_client
|
|
766
|
+
client = get_docker_client()
|
|
767
|
+
sanitized_id = _re.sub(r'[^a-zA-Z0-9_.-]', '-', chat_id)
|
|
768
|
+
container_name = f"owui-chat-{sanitized_id}"
|
|
769
|
+
try:
|
|
770
|
+
c = client.containers.get(container_name)
|
|
771
|
+
c.reload()
|
|
772
|
+
if c.status == "running":
|
|
773
|
+
return c
|
|
774
|
+
except Exception:
|
|
775
|
+
pass
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _get_container_stopped(chat_id: str):
|
|
780
|
+
"""Find a stopped (but not removed) container for this chat."""
|
|
781
|
+
import re as _re
|
|
782
|
+
from docker_manager import get_docker_client
|
|
783
|
+
client = get_docker_client()
|
|
784
|
+
sanitized_id = _re.sub(r'[^a-zA-Z0-9_.-]', '-', chat_id)
|
|
785
|
+
container_name = f"owui-chat-{sanitized_id}"
|
|
786
|
+
try:
|
|
787
|
+
c = client.containers.get(container_name)
|
|
788
|
+
c.reload()
|
|
789
|
+
if c.status in ("exited", "created"):
|
|
790
|
+
return c
|
|
791
|
+
except Exception:
|
|
792
|
+
pass
|
|
793
|
+
return None
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@app.get("/terminal/{chat_id}/status", tags=["Terminal"])
|
|
797
|
+
async def terminal_status(chat_id: str, response: Response):
|
|
798
|
+
"""Check if ttyd terminal is available in the chat's container."""
|
|
799
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
800
|
+
response.headers["Cache-Control"] = "no-cache, no-store"
|
|
801
|
+
container_ip = get_container_cdp_address(chat_id)
|
|
802
|
+
if not container_ip:
|
|
803
|
+
# Check if container exists but is stopped
|
|
804
|
+
container = _get_container_stopped(chat_id)
|
|
805
|
+
if container:
|
|
806
|
+
return {"active": False, "container_stopped": True}
|
|
807
|
+
# Container removed — check if .meta.json exists for resurrection
|
|
808
|
+
from docker_manager import load_container_meta
|
|
809
|
+
if load_container_meta(chat_id):
|
|
810
|
+
return {"active": False, "meta_exists": True}
|
|
811
|
+
return {"active": False}
|
|
812
|
+
try:
|
|
813
|
+
async with aiohttp.ClientSession() as session:
|
|
814
|
+
async with session.get(
|
|
815
|
+
f"http://{container_ip}:7681/",
|
|
816
|
+
timeout=aiohttp.ClientTimeout(total=2)
|
|
817
|
+
) as resp:
|
|
818
|
+
return {"active": resp.status == 200}
|
|
819
|
+
except Exception:
|
|
820
|
+
return {"active": False}
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
class StartTtydRequest(BaseModel):
|
|
824
|
+
dangerous_mode: bool = False
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
@app.post("/terminal/{chat_id}/start-ttyd", tags=["Terminal"])
|
|
828
|
+
async def start_ttyd(chat_id: str, body: StartTtydRequest = Body(default=StartTtydRequest())):
|
|
829
|
+
"""Start ttyd + tmux in the container (lazy start).
|
|
830
|
+
|
|
831
|
+
Uses tmux -A (attach-if-exists) so reconnections work without errors.
|
|
832
|
+
If dangerous_mode=True, sets NO_AUTOSTART=1 so .bashrc skips auto-launch —
|
|
833
|
+
the frontend will inject `claude --dangerously-skip-permissions` after connecting.
|
|
834
|
+
"""
|
|
835
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
836
|
+
from docker_manager import _execute_bash
|
|
837
|
+
container = _get_container_for_terminal(chat_id)
|
|
838
|
+
if not container:
|
|
839
|
+
raise HTTPException(404, "Container not running")
|
|
840
|
+
# Check if already running
|
|
841
|
+
check = await asyncio.to_thread(
|
|
842
|
+
_execute_bash, container,
|
|
843
|
+
"pgrep -x ttyd > /dev/null 2>&1 && echo RUNNING || echo STOPPED", 5)
|
|
844
|
+
if "RUNNING" in check.get("output", ""):
|
|
845
|
+
return {"started": False, "already_running": True}
|
|
846
|
+
# Start ttyd + tmux in background (-A = attach if session exists, create if not)
|
|
847
|
+
env_prefix = "NO_AUTOSTART=1 " if body.dangerous_mode else ""
|
|
848
|
+
await asyncio.to_thread(
|
|
849
|
+
_execute_bash, container,
|
|
850
|
+
f"cd /home/assistant && echo 'set -g mouse on' > ~/.tmux.conf && LANG=C.UTF-8 {env_prefix}nohup ttyd -W -p 7681 tmux -u new-session -A -s main bash > /dev/null 2>&1 &", 5)
|
|
851
|
+
# Wait briefly for ttyd to start
|
|
852
|
+
await asyncio.sleep(0.5)
|
|
853
|
+
return {"started": True}
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
@app.post("/terminal/{chat_id}/stop-ttyd", tags=["Terminal"])
|
|
857
|
+
async def stop_ttyd(chat_id: str):
|
|
858
|
+
"""Stop ttyd + tmux in the container so next start is fresh (with .bashrc autostart)."""
|
|
859
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
860
|
+
from docker_manager import _execute_bash
|
|
861
|
+
container = _get_container_for_terminal(chat_id)
|
|
862
|
+
if not container:
|
|
863
|
+
raise HTTPException(404, "Container not running")
|
|
864
|
+
await asyncio.to_thread(
|
|
865
|
+
_execute_bash, container,
|
|
866
|
+
"pkill -x ttyd 2>/dev/null; tmux kill-server 2>/dev/null; true", 5)
|
|
867
|
+
return {"stopped": True}
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
@app.post("/terminal/{chat_id}/restart-container", tags=["Terminal"])
|
|
871
|
+
async def restart_container(chat_id: str):
|
|
872
|
+
"""Restart a stopped container. Handles dead networks after deploy."""
|
|
873
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
874
|
+
container = _get_container_stopped(chat_id)
|
|
875
|
+
if not container:
|
|
876
|
+
raise HTTPException(404, "No stopped container found")
|
|
877
|
+
try:
|
|
878
|
+
from docker_manager import get_docker_client, _get_compose_network_name
|
|
879
|
+
client = get_docker_client()
|
|
880
|
+
|
|
881
|
+
# Disconnect from dead networks (left over after docker-compose down/up)
|
|
882
|
+
container.reload()
|
|
883
|
+
old_nets = list(container.attrs.get("NetworkSettings", {}).get("Networks", {}).keys())
|
|
884
|
+
for net_name in old_nets:
|
|
885
|
+
try:
|
|
886
|
+
net = client.networks.get(net_name)
|
|
887
|
+
net.disconnect(container, force=True)
|
|
888
|
+
except Exception:
|
|
889
|
+
pass # Network already dead — ignore
|
|
890
|
+
|
|
891
|
+
# Connect to current compose network (force refresh — network ID changed after deploy)
|
|
892
|
+
compose_net = _get_compose_network_name(force_refresh=True)
|
|
893
|
+
if compose_net:
|
|
894
|
+
try:
|
|
895
|
+
net = client.networks.get(compose_net)
|
|
896
|
+
net.connect(container)
|
|
897
|
+
except Exception as e:
|
|
898
|
+
print(f"[RESTART] Warning: could not connect to {compose_net}: {e}")
|
|
899
|
+
|
|
900
|
+
await asyncio.to_thread(container.start)
|
|
901
|
+
await asyncio.sleep(2) # Wait for entrypoint
|
|
902
|
+
return {"restarted": True}
|
|
903
|
+
except Exception as e:
|
|
904
|
+
raise HTTPException(500, f"Failed to restart: {e}")
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
@app.post("/terminal/{chat_id}/resurrect-container", tags=["Terminal"])
|
|
908
|
+
async def resurrect_container(chat_id: str):
|
|
909
|
+
"""Recreate a removed container using saved .meta.json and existing host data.
|
|
910
|
+
|
|
911
|
+
Used when container was removed by cron but /data/{chat_id}/.meta.json
|
|
912
|
+
still exists. Restores user identity (email, name, MCP servers) from saved metadata.
|
|
913
|
+
"""
|
|
914
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
915
|
+
import re as _re
|
|
916
|
+
from docker_manager import load_container_meta, _create_container, _ensure_gitlab_token
|
|
917
|
+
from context_vars import (
|
|
918
|
+
current_chat_id, current_user_email, current_user_name,
|
|
919
|
+
current_mcp_servers,
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
# Guard: container must not already exist
|
|
923
|
+
existing = _get_container_for_terminal(chat_id) or _get_container_stopped(chat_id)
|
|
924
|
+
if existing:
|
|
925
|
+
raise HTTPException(409, "Container already exists. Use restart-container instead.")
|
|
926
|
+
|
|
927
|
+
meta = load_container_meta(chat_id)
|
|
928
|
+
if not meta:
|
|
929
|
+
raise HTTPException(404, "No .meta.json found for this chat")
|
|
930
|
+
|
|
931
|
+
# Set context vars from saved metadata (non-secret only).
|
|
932
|
+
# Tokens (ANTHROPIC_AUTH_TOKEN, VISION_*) come from computer-use-orchestrator ENV
|
|
933
|
+
# via fallback in _create_container.
|
|
934
|
+
current_chat_id.set(chat_id)
|
|
935
|
+
user_email = meta.get("user_email", "")
|
|
936
|
+
user_name = meta.get("user_name", "")
|
|
937
|
+
mcp_servers = meta.get("mcp_servers", "")
|
|
938
|
+
if user_email:
|
|
939
|
+
current_user_email.set(user_email)
|
|
940
|
+
if user_name:
|
|
941
|
+
current_user_name.set(user_name)
|
|
942
|
+
if mcp_servers:
|
|
943
|
+
current_mcp_servers.set(mcp_servers)
|
|
944
|
+
|
|
945
|
+
# Fetch fresh GitLab token by email (never stored on disk)
|
|
946
|
+
await _ensure_gitlab_token()
|
|
947
|
+
|
|
948
|
+
sanitized_id = _re.sub(r'[^a-zA-Z0-9_.-]', '-', chat_id)
|
|
949
|
+
container_name = f"owui-chat-{sanitized_id}"
|
|
950
|
+
|
|
951
|
+
try:
|
|
952
|
+
print(f"[RESURRECT] Recreating {container_name} for {user_email}")
|
|
953
|
+
await asyncio.to_thread(_create_container, chat_id, container_name)
|
|
954
|
+
await asyncio.sleep(2) # Wait for entrypoint
|
|
955
|
+
return {"resurrected": True, "user_email": user_email}
|
|
956
|
+
except Exception as e:
|
|
957
|
+
raise HTTPException(500, f"Failed to resurrect container: {e}")
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
@app.get("/terminal/{chat_id}/sessions", tags=["Terminal"])
|
|
961
|
+
async def terminal_sessions(chat_id: str, response: Response):
|
|
962
|
+
"""List Claude Code JSONL sessions from the container."""
|
|
963
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
964
|
+
response.headers["Cache-Control"] = "no-cache, no-store"
|
|
965
|
+
from docker_manager import _execute_bash
|
|
966
|
+
container = _get_container_for_terminal(chat_id)
|
|
967
|
+
if not container:
|
|
968
|
+
return {"sessions": []}
|
|
969
|
+
result = await asyncio.to_thread(
|
|
970
|
+
_execute_bash, container,
|
|
971
|
+
r"""for f in $(ls -t /home/assistant/.claude/projects/-home-assistant/*.jsonl /root/.claude/projects/-home-assistant/*.jsonl 2>/dev/null | sort -u | head -20); do
|
|
972
|
+
sid=$(basename "$f" .jsonl)
|
|
973
|
+
model=$(grep -o '"model":"[^"]*"' "$f" 2>/dev/null | head -1 | cut -d'"' -f4)
|
|
974
|
+
ts=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null)
|
|
975
|
+
label=$(grep -m1 '"type":"user"' "$f" 2>/dev/null | sed 's/.*"content":\s*"//' | sed 's/".*//' | cut -c1-80)
|
|
976
|
+
echo "$sid|$model|$ts|$label"
|
|
977
|
+
done""", 10)
|
|
978
|
+
sessions = []
|
|
979
|
+
for line in (result.get("output", "") or "").strip().split("\n"):
|
|
980
|
+
if not line.strip() or "|" not in line:
|
|
981
|
+
continue
|
|
982
|
+
parts = line.strip().split("|", 3)
|
|
983
|
+
if len(parts) >= 3:
|
|
984
|
+
sessions.append({
|
|
985
|
+
"session_id": parts[0],
|
|
986
|
+
"model": parts[1] or "unknown",
|
|
987
|
+
"timestamp": float(parts[2]) if parts[2] else 0,
|
|
988
|
+
"label": parts[3].strip() if len(parts) > 3 else "",
|
|
989
|
+
})
|
|
990
|
+
return {"sessions": sessions}
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
@app.get("/terminal/{chat_id}/processes", tags=["Terminal"])
|
|
994
|
+
async def terminal_processes(chat_id: str, response: Response):
|
|
995
|
+
"""List running Claude processes in the container."""
|
|
996
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
997
|
+
response.headers["Cache-Control"] = "no-cache, no-store"
|
|
998
|
+
from docker_manager import _execute_bash
|
|
999
|
+
container = _get_container_for_terminal(chat_id)
|
|
1000
|
+
if not container:
|
|
1001
|
+
return {"processes": []}
|
|
1002
|
+
result = await asyncio.to_thread(
|
|
1003
|
+
_execute_bash, container,
|
|
1004
|
+
r"ps -eo pid,etimes,args --no-headers 2>/dev/null | grep '[c]laude-code/cli.js' | head -10", 10)
|
|
1005
|
+
processes = []
|
|
1006
|
+
for line in (result.get("output", "") or "").strip().split("\n"):
|
|
1007
|
+
line = line.strip()
|
|
1008
|
+
if not line:
|
|
1009
|
+
continue
|
|
1010
|
+
parts = line.split(None, 2)
|
|
1011
|
+
if len(parts) >= 3:
|
|
1012
|
+
try:
|
|
1013
|
+
pid = int(parts[0])
|
|
1014
|
+
elapsed_sec = int(parts[1])
|
|
1015
|
+
command = parts[2]
|
|
1016
|
+
processes.append({
|
|
1017
|
+
"pid": pid,
|
|
1018
|
+
"elapsed_minutes": elapsed_sec // 60,
|
|
1019
|
+
"command": command[:100], # truncate for display
|
|
1020
|
+
})
|
|
1021
|
+
except (ValueError, IndexError):
|
|
1022
|
+
continue
|
|
1023
|
+
return {"processes": processes}
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
@app.post("/terminal/{chat_id}/processes/{pid}/kill", tags=["Terminal"])
|
|
1027
|
+
async def kill_terminal_process(chat_id: str, pid: int):
|
|
1028
|
+
"""Kill a Claude process in the container by PID."""
|
|
1029
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
1030
|
+
container = _get_container_for_terminal(chat_id)
|
|
1031
|
+
if not container:
|
|
1032
|
+
raise HTTPException(404, "Container not running")
|
|
1033
|
+
# Validate: only kill claude processes (not PID 1 or random processes)
|
|
1034
|
+
def _do_kill():
|
|
1035
|
+
check = container.exec_run(
|
|
1036
|
+
["bash", "-c", f"ps -p {pid} -o args= 2>/dev/null | grep -q claude && echo OK || echo DENIED"],
|
|
1037
|
+
demux=True)
|
|
1038
|
+
stdout = (check.output[0] or b"").decode().strip() if check.output else ""
|
|
1039
|
+
if "OK" not in stdout:
|
|
1040
|
+
return False
|
|
1041
|
+
# SIGTERM first, then SIGKILL if still alive after 2s
|
|
1042
|
+
container.exec_run(["kill", "-15", str(pid)])
|
|
1043
|
+
import time; time.sleep(2)
|
|
1044
|
+
alive = container.exec_run(
|
|
1045
|
+
["bash", "-c", f"kill -0 {pid} 2>/dev/null && echo ALIVE || echo DEAD"],
|
|
1046
|
+
demux=True)
|
|
1047
|
+
alive_out = (alive.output[0] or b"").decode().strip() if alive.output else ""
|
|
1048
|
+
if "ALIVE" in alive_out:
|
|
1049
|
+
container.exec_run(["kill", "-9", str(pid)])
|
|
1050
|
+
return True
|
|
1051
|
+
killed = await asyncio.to_thread(_do_kill)
|
|
1052
|
+
if not killed:
|
|
1053
|
+
raise HTTPException(400, "Can only kill Claude processes")
|
|
1054
|
+
return {"killed": True, "pid": pid}
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
@app.get("/terminal/{chat_id}/heartbeat", tags=["Terminal"])
|
|
1058
|
+
async def terminal_heartbeat(chat_id: str):
|
|
1059
|
+
"""Reset container idle timer. Called by JS every 2 min while page is open."""
|
|
1060
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
1061
|
+
container = _get_container_for_terminal(chat_id)
|
|
1062
|
+
if container:
|
|
1063
|
+
from docker_manager import _reset_shutdown_timer
|
|
1064
|
+
await asyncio.to_thread(_reset_shutdown_timer, container)
|
|
1065
|
+
return {"ok": True}
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
@app.websocket("/terminal/{chat_id}/ws")
|
|
1069
|
+
async def terminal_ws_proxy(websocket: WebSocket, chat_id: str):
|
|
1070
|
+
"""Bidirectional WebSocket proxy — connects xterm.js to container's ttyd."""
|
|
1071
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
1072
|
+
container_ip = get_container_cdp_address(chat_id)
|
|
1073
|
+
if not container_ip:
|
|
1074
|
+
await websocket.close(code=1008, reason="Container not found")
|
|
1075
|
+
return
|
|
1076
|
+
|
|
1077
|
+
await websocket.accept(subprotocol="tty")
|
|
1078
|
+
|
|
1079
|
+
backend_url = f"ws://{container_ip}:7681/ws"
|
|
1080
|
+
|
|
1081
|
+
try:
|
|
1082
|
+
async with aiohttp.ClientSession() as session:
|
|
1083
|
+
async with session.ws_connect(backend_url, protocols=["tty"]) as backend_ws:
|
|
1084
|
+
async def forward_client_to_backend():
|
|
1085
|
+
try:
|
|
1086
|
+
while True:
|
|
1087
|
+
msg = await websocket.receive()
|
|
1088
|
+
if msg["type"] == "websocket.disconnect":
|
|
1089
|
+
break
|
|
1090
|
+
if msg.get("bytes"):
|
|
1091
|
+
await backend_ws.send_bytes(msg["bytes"])
|
|
1092
|
+
elif msg.get("text"):
|
|
1093
|
+
await backend_ws.send_str(msg["text"])
|
|
1094
|
+
except WebSocketDisconnect:
|
|
1095
|
+
pass
|
|
1096
|
+
except Exception:
|
|
1097
|
+
pass
|
|
1098
|
+
finally:
|
|
1099
|
+
await backend_ws.close()
|
|
1100
|
+
|
|
1101
|
+
async def forward_backend_to_client():
|
|
1102
|
+
try:
|
|
1103
|
+
async for msg in backend_ws:
|
|
1104
|
+
if msg.type == aiohttp.WSMsgType.BINARY:
|
|
1105
|
+
await websocket.send_bytes(msg.data)
|
|
1106
|
+
elif msg.type == aiohttp.WSMsgType.TEXT:
|
|
1107
|
+
await websocket.send_text(msg.data)
|
|
1108
|
+
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
|
1109
|
+
break
|
|
1110
|
+
except Exception:
|
|
1111
|
+
pass
|
|
1112
|
+
finally:
|
|
1113
|
+
try:
|
|
1114
|
+
await websocket.close()
|
|
1115
|
+
except Exception:
|
|
1116
|
+
pass
|
|
1117
|
+
|
|
1118
|
+
done, pending = await asyncio.wait(
|
|
1119
|
+
[
|
|
1120
|
+
asyncio.create_task(forward_client_to_backend()),
|
|
1121
|
+
asyncio.create_task(forward_backend_to_client()),
|
|
1122
|
+
],
|
|
1123
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
1124
|
+
)
|
|
1125
|
+
for task in pending:
|
|
1126
|
+
task.cancel()
|
|
1127
|
+
except Exception:
|
|
1128
|
+
try:
|
|
1129
|
+
await websocket.close(code=1011, reason="Backend connection failed")
|
|
1130
|
+
except Exception:
|
|
1131
|
+
pass
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
@app.get("/preview/{chat_id}", response_class=HTMLResponse, tags=["Files"])
|
|
1135
|
+
async def preview_page(chat_id: str, response: Response):
|
|
1136
|
+
"""
|
|
1137
|
+
Self-contained file preview SPA.
|
|
1138
|
+
Shows output files with auto-refresh, file navigation, and type-specific preview.
|
|
1139
|
+
Designed to be embedded in an iframe (Open WebUI Artifacts panel).
|
|
1140
|
+
"""
|
|
1141
|
+
chat_id = sanitize_chat_id(chat_id)
|
|
1142
|
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
1143
|
+
# Use relative URLs so it works behind HTTPS reverse proxy
|
|
1144
|
+
api_url = f"/api/outputs/{chat_id}"
|
|
1145
|
+
files_base = f"/files/{chat_id}"
|
|
1146
|
+
|
|
1147
|
+
return _generate_preview_html(chat_id, api_url, files_base)
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _generate_preview_html(chat_id: str, api_url: str, files_base: str) -> str:
|
|
1151
|
+
"""Generate the preview SPA HTML page."""
|
|
1152
|
+
return f'''<!DOCTYPE html>
|
|
1153
|
+
<html lang="en">
|
|
1154
|
+
<head>
|
|
1155
|
+
<meta charset="UTF-8">
|
|
1156
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1157
|
+
<title>File Preview</title>
|
|
1158
|
+
<link rel="stylesheet" href="/static/preview.css">
|
|
1159
|
+
<link rel="stylesheet" href="/static/github.min.css" media="(prefers-color-scheme: light)">
|
|
1160
|
+
<link rel="stylesheet" href="/static/github-dark.min.css" media="(prefers-color-scheme: dark)">
|
|
1161
|
+
<link rel="stylesheet" href="/static/katex/katex.min.css">
|
|
1162
|
+
<link rel="stylesheet" href="/static/xterm.css">
|
|
1163
|
+
<script src="/static/highlight.min.js"></script>
|
|
1164
|
+
<script src="/static/highlightjs-line-numbers.min.js"></script>
|
|
1165
|
+
<script src="/static/marked.min.js"></script>
|
|
1166
|
+
<script src="/static/xterm.min.js"></script>
|
|
1167
|
+
<script src="/static/xterm-addon-fit.min.js"></script>
|
|
1168
|
+
<script src="/static/xterm-addon-web-links.min.js"></script>
|
|
1169
|
+
</head>
|
|
1170
|
+
<body>
|
|
1171
|
+
<div id="app"></div>
|
|
1172
|
+
<script>
|
|
1173
|
+
window.__CONFIG__ = {{
|
|
1174
|
+
apiUrl: {json.dumps(api_url)},
|
|
1175
|
+
filesBase: {json.dumps(files_base)},
|
|
1176
|
+
chatId: {json.dumps(chat_id)}
|
|
1177
|
+
}};
|
|
1178
|
+
// Heartbeat: keep container alive while page is open (every 2 min)
|
|
1179
|
+
setInterval(function() {{ fetch('/terminal/' + {json.dumps(chat_id)} + '/heartbeat').catch(function(){{}}); }}, 120000);
|
|
1180
|
+
</script>
|
|
1181
|
+
<script type="module" src="/static/preview.js"></script>
|
|
1182
|
+
</body>
|
|
1183
|
+
</html>'''
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
@app.get("/system-prompt", response_class=PlainTextResponse, tags=["System"])
|
|
1187
|
+
async def system_prompt(
|
|
1188
|
+
request: Request,
|
|
1189
|
+
chat_id: Optional[str] = None,
|
|
1190
|
+
file_base_url: Optional[str] = None,
|
|
1191
|
+
archive_url: Optional[str] = None,
|
|
1192
|
+
user_email: Optional[str] = None,
|
|
1193
|
+
):
|
|
1194
|
+
"""
|
|
1195
|
+
Get Computer Use system prompt for AI integrations.
|
|
1196
|
+
|
|
1197
|
+
Tier 6 — backward-compat HTTP endpoint. Open WebUI filter fetches this URL
|
|
1198
|
+
and injects the response into the LLM's system message. Every other
|
|
1199
|
+
consumer (Agents SDK, Inspector, Claude Desktop) should prefer the native
|
|
1200
|
+
MCP tiers (InitializeResult.instructions on initialize, or just
|
|
1201
|
+
/home/assistant/README.md inside the sandbox).
|
|
1202
|
+
|
|
1203
|
+
chat_id resolution, in priority order:
|
|
1204
|
+
1. request header (X-Chat-Id / X-User-Email, plus X-OpenWebUI-* aliases
|
|
1205
|
+
to match the rest of the server — see mcp_tools.set_context_from_headers)
|
|
1206
|
+
2. `chat_id` query param
|
|
1207
|
+
3. last path segment of the deprecated `file_base_url` query param
|
|
1208
|
+
(kept for backwards compat with pre-v4.0.0 integrations; see below)
|
|
1209
|
+
4. "default"
|
|
1210
|
+
|
|
1211
|
+
`file_base_url` / `archive_url` are deprecated query params from pre-v4.0.0:
|
|
1212
|
+
before the server owned `PUBLIC_BASE_URL`, clients passed their own
|
|
1213
|
+
browser-facing URLs here. Now the server always emits PUBLIC_BASE_URL/files/
|
|
1214
|
+
{chat_id} and we only still read `file_base_url` to extract the trailing
|
|
1215
|
+
chat_id for legacy callers. `archive_url` is ignored entirely — the server
|
|
1216
|
+
derives archive URLs from PUBLIC_BASE_URL + chat_id.
|
|
1217
|
+
"""
|
|
1218
|
+
def _header(*names: str) -> Optional[str]:
|
|
1219
|
+
for n in names:
|
|
1220
|
+
v = request.headers.get(n)
|
|
1221
|
+
if v:
|
|
1222
|
+
return v
|
|
1223
|
+
return None
|
|
1224
|
+
|
|
1225
|
+
def _extract_chat_id_from_legacy_url(url: Optional[str]) -> Optional[str]:
|
|
1226
|
+
if not url:
|
|
1227
|
+
return None
|
|
1228
|
+
tail = url.rstrip("/").rsplit("/", 1)[-1]
|
|
1229
|
+
return tail or None
|
|
1230
|
+
|
|
1231
|
+
raw_chat_id = (
|
|
1232
|
+
_header("x-chat-id", "x-openwebui-chat-id")
|
|
1233
|
+
or chat_id
|
|
1234
|
+
or _extract_chat_id_from_legacy_url(file_base_url)
|
|
1235
|
+
)
|
|
1236
|
+
# Sanitize at the boundary — same invariant other endpoints in this
|
|
1237
|
+
# file enforce (see /files/{chat_id}/archive). Without this,
|
|
1238
|
+
# untrusted chat_id from headers/query lands in /system-prompt URLs
|
|
1239
|
+
# via the renderer.
|
|
1240
|
+
effective_chat_id = sanitize_chat_id(raw_chat_id) if raw_chat_id else None
|
|
1241
|
+
effective_user_email = (
|
|
1242
|
+
_header("x-user-email", "x-openwebui-user-email") or user_email
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
# Single rendering path — shared cache with Tiers 2, 4, 5.
|
|
1246
|
+
# chat_id=None preserves the legacy diagnostic path: external callers
|
|
1247
|
+
# (n8n et al.) hitting /system-prompt with no params get the template
|
|
1248
|
+
# back with placeholders intact and substitute them themselves.
|
|
1249
|
+
result = await render_system_prompt(
|
|
1250
|
+
effective_chat_id,
|
|
1251
|
+
effective_user_email,
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
# Public URL is owned by the server. Expose via response header so the
|
|
1255
|
+
# Open WebUI filter can decorate outlet() with browser-facing links without
|
|
1256
|
+
# needing its own Valve — its ORCHESTRATOR_URL is internal-only.
|
|
1257
|
+
return PlainTextResponse(
|
|
1258
|
+
content=result,
|
|
1259
|
+
headers={"X-Public-Base-URL": PUBLIC_BASE_URL},
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
@app.get("/skill-mounts", tags=["System"])
|
|
1264
|
+
async def skill_mounts_endpoint(user_email: Optional[str] = None):
|
|
1265
|
+
"""
|
|
1266
|
+
Get Docker volume mounts for user-uploaded skills.
|
|
1267
|
+
|
|
1268
|
+
Returns dict of {host_path: {"bind": container_path, "mode": "ro"}}
|
|
1269
|
+
for use by computer_use_tools.py when creating containers.
|
|
1270
|
+
"""
|
|
1271
|
+
if not user_email:
|
|
1272
|
+
return {}
|
|
1273
|
+
skills = await skill_manager.get_user_skills(user_email)
|
|
1274
|
+
for s in skills:
|
|
1275
|
+
if s.category == "user":
|
|
1276
|
+
await skill_manager.ensure_skill_cached(s)
|
|
1277
|
+
return skill_manager.get_skill_mounts(skills)
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
@app.get("/skill-list", response_class=PlainTextResponse, tags=["System"])
|
|
1281
|
+
async def skill_list_endpoint(
|
|
1282
|
+
user_email: Optional[str] = None,
|
|
1283
|
+
format: str = "sub_agent",
|
|
1284
|
+
):
|
|
1285
|
+
"""
|
|
1286
|
+
Get skills list as text for sub-agent prompt.
|
|
1287
|
+
|
|
1288
|
+
Returns formatted text: "- name: location - description" per line.
|
|
1289
|
+
"""
|
|
1290
|
+
skills = await skill_manager.get_user_skills(user_email)
|
|
1291
|
+
if format == "sub_agent":
|
|
1292
|
+
return skill_manager.build_sub_agent_skills_text(skills)
|
|
1293
|
+
return skill_manager.build_sub_agent_skills_text(skills)
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
@app.get("/health", tags=["System"])
|
|
1297
|
+
async def health():
|
|
1298
|
+
"""Health check for monitoring"""
|
|
1299
|
+
return {"status": "healthy"}
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
# ============================================================================
|
|
1303
|
+
# Skill Usage Stats
|
|
1304
|
+
# ============================================================================
|
|
1305
|
+
|
|
1306
|
+
CENTRAL_LOG = BASE_DATA_DIR / "skill-usage-central.jsonl"
|
|
1307
|
+
_central_log_lock = asyncio.Lock()
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def _harvest_and_get_stats() -> dict:
|
|
1311
|
+
"""
|
|
1312
|
+
Scan per-chat .skill-usage.jsonl files, append new events to central log,
|
|
1313
|
+
then aggregate stats from the central log.
|
|
1314
|
+
"""
|
|
1315
|
+
seen: set = set()
|
|
1316
|
+
|
|
1317
|
+
# Load already-seen events from central log (dedup key: ts+chat_id+skill)
|
|
1318
|
+
if CENTRAL_LOG.exists():
|
|
1319
|
+
with open(CENTRAL_LOG) as f:
|
|
1320
|
+
for line in f:
|
|
1321
|
+
try:
|
|
1322
|
+
ev = json.loads(line)
|
|
1323
|
+
seen.add((ev["ts"], ev.get("chat_id", ""), ev.get("skill", "")))
|
|
1324
|
+
except Exception:
|
|
1325
|
+
pass
|
|
1326
|
+
|
|
1327
|
+
# Harvest new events from per-chat logs
|
|
1328
|
+
new_events: list = []
|
|
1329
|
+
for log_path in BASE_DATA_DIR.glob("*/outputs/.skill-usage.jsonl"):
|
|
1330
|
+
try:
|
|
1331
|
+
with open(log_path) as f:
|
|
1332
|
+
for line in f:
|
|
1333
|
+
try:
|
|
1334
|
+
ev = json.loads(line.strip())
|
|
1335
|
+
key = (ev["ts"], ev.get("chat_id", ""), ev.get("skill", ""))
|
|
1336
|
+
if key not in seen:
|
|
1337
|
+
seen.add(key)
|
|
1338
|
+
new_events.append(ev)
|
|
1339
|
+
except Exception:
|
|
1340
|
+
pass
|
|
1341
|
+
except OSError:
|
|
1342
|
+
pass
|
|
1343
|
+
|
|
1344
|
+
# Persist new events to central log
|
|
1345
|
+
if new_events:
|
|
1346
|
+
with open(CENTRAL_LOG, "a") as f:
|
|
1347
|
+
for ev in new_events:
|
|
1348
|
+
f.write(json.dumps(ev, ensure_ascii=False) + "\n")
|
|
1349
|
+
|
|
1350
|
+
# Aggregate stats from central log
|
|
1351
|
+
stats: dict = {}
|
|
1352
|
+
if CENTRAL_LOG.exists():
|
|
1353
|
+
with open(CENTRAL_LOG) as f:
|
|
1354
|
+
for line in f:
|
|
1355
|
+
try:
|
|
1356
|
+
ev = json.loads(line)
|
|
1357
|
+
skill = ev.get("skill", "unknown")
|
|
1358
|
+
email = ev.get("email", "unknown")
|
|
1359
|
+
s = stats.setdefault(skill, {"total": 0, "by_email": {}})
|
|
1360
|
+
s["total"] += 1
|
|
1361
|
+
s["by_email"][email] = s["by_email"].get(email, 0) + 1
|
|
1362
|
+
except Exception:
|
|
1363
|
+
pass
|
|
1364
|
+
|
|
1365
|
+
return {
|
|
1366
|
+
"skills": stats,
|
|
1367
|
+
"total_events": sum(s["total"] for s in stats.values()),
|
|
1368
|
+
"harvested_new": len(new_events),
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
@app.get("/api/skill-stats", tags=["System"])
|
|
1373
|
+
async def skill_stats(x_internal_api_key: Optional[str] = Header(default=None)):
|
|
1374
|
+
"""
|
|
1375
|
+
Aggregate skill usage stats from all chat containers.
|
|
1376
|
+
|
|
1377
|
+
Scans per-chat .skill-usage.jsonl files written by the inotify watcher
|
|
1378
|
+
running inside containers, persists new events to a central durable log,
|
|
1379
|
+
and returns aggregated counts per skill and user email.
|
|
1380
|
+
|
|
1381
|
+
Requires X-Internal-Api-Key header (matches MCP_TOKENS_API_KEY env var).
|
|
1382
|
+
"""
|
|
1383
|
+
api_key = os.getenv("MCP_TOKENS_API_KEY", "")
|
|
1384
|
+
if api_key and x_internal_api_key != api_key:
|
|
1385
|
+
raise HTTPException(status_code=403, detail="Forbidden")
|
|
1386
|
+
async with _central_log_lock:
|
|
1387
|
+
result = await asyncio.get_event_loop().run_in_executor(None, _harvest_and_get_stats)
|
|
1388
|
+
return result
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
# ============================================================================
|
|
1392
|
+
# Runtime introspection (Phase 9.5 — Preview SPA multi-CLI surface)
|
|
1393
|
+
# ============================================================================
|
|
1394
|
+
|
|
1395
|
+
@app.get("/api/runtime/cli", tags=["System"])
|
|
1396
|
+
async def runtime_cli(response: Response):
|
|
1397
|
+
"""Return the active sub-agent CLI runtime.
|
|
1398
|
+
|
|
1399
|
+
Pure additive endpoint — no auth, no side effects, no breakage of the
|
|
1400
|
+
existing MCP contract. Surfaces what `SUBAGENT_CLI` resolved to at
|
|
1401
|
+
orchestrator boot (`docker_manager.SUBAGENT_CLI`) plus the per-CLI
|
|
1402
|
+
default model so the Preview SPA can render an active-CLI badge.
|
|
1403
|
+
|
|
1404
|
+
Cache-Control: no-store — the value is fixed for the orchestrator
|
|
1405
|
+
lifetime, but operators flipping `SUBAGENT_CLI` in `.env` and
|
|
1406
|
+
restarting must see the new value immediately on next page load.
|
|
1407
|
+
"""
|
|
1408
|
+
response.headers["Cache-Control"] = "no-store"
|
|
1409
|
+
# Resolve through cli_runtime so the badge reflects the SAME default
|
|
1410
|
+
# the dispatcher will actually run, including all env-var overrides
|
|
1411
|
+
# (CODEX_SUB_AGENT_DEFAULT_MODEL, OPENCODE_MODEL, etc.). Calling
|
|
1412
|
+
# resolve_subagent_model("", cli) returns the (model_id, display_name)
|
|
1413
|
+
# tuple — we surface model_id since that's what hits the wire.
|
|
1414
|
+
# CR PR#76 finding #1: do not duplicate the per-CLI fallback table here.
|
|
1415
|
+
from cli_runtime import Cli, resolve_cli, resolve_subagent_model
|
|
1416
|
+
cli = resolve_cli()
|
|
1417
|
+
# Phase 2: opencode/codex no longer have a hardcoded fallback. When the
|
|
1418
|
+
# operator hasn't set a per-CLI default env, resolve_subagent_model raises
|
|
1419
|
+
# a ValueError pointing them at list-subagent-models. Surface that as
|
|
1420
|
+
# default_model=null so the badge can render "no default — set
|
|
1421
|
+
# <CLI>_SUB_AGENT_DEFAULT_MODEL" instead of crashing the endpoint.
|
|
1422
|
+
try:
|
|
1423
|
+
model_id, _display = resolve_subagent_model("", cli)
|
|
1424
|
+
except ValueError:
|
|
1425
|
+
model_id = None
|
|
1426
|
+
return {
|
|
1427
|
+
"cli": cli.value,
|
|
1428
|
+
"default_model": model_id,
|
|
1429
|
+
"supports_cost": cli == Cli.CLAUDE,
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
# ============================================================================
|
|
1434
|
+
# MCP Endpoint Integration
|
|
1435
|
+
# ============================================================================
|
|
1436
|
+
|
|
1437
|
+
# MCP requires initialization via lifespan context manager
|
|
1438
|
+
# We'll use a custom SSE-based approach for better compatibility
|
|
1439
|
+
|
|
1440
|
+
_mcp_server = None
|
|
1441
|
+
_mcp_set_context = None
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def _init_mcp():
|
|
1445
|
+
"""Initialize MCP server (lazy load).
|
|
1446
|
+
|
|
1447
|
+
Note: ImportError used to be caught here. After the lifespan rewrite that
|
|
1448
|
+
crashes loud on missing mcp_tools, that catch is dead code — if the
|
|
1449
|
+
import would fail, the server never gets past startup. Keeping the
|
|
1450
|
+
import unguarded so any stale assumption blows up at the call site
|
|
1451
|
+
rather than silently returning None.
|
|
1452
|
+
"""
|
|
1453
|
+
global _mcp_server, _mcp_set_context
|
|
1454
|
+
if _mcp_server is None:
|
|
1455
|
+
from mcp_tools import mcp, set_context_from_headers
|
|
1456
|
+
_mcp_server = mcp
|
|
1457
|
+
_mcp_set_context = set_context_from_headers
|
|
1458
|
+
return _mcp_server, _mcp_set_context
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
# =============================================================================
|
|
1462
|
+
# Mount native MCP Streamable HTTP app (SSE + progress notifications)
|
|
1463
|
+
# =============================================================================
|
|
1464
|
+
# Replaces the old custom JSON-RPC handler with FastMCP's native Starlette app.
|
|
1465
|
+
# This enables:
|
|
1466
|
+
# - SSE streaming for real-time progress updates via ctx.report_progress()
|
|
1467
|
+
# - Standard MCP protocol compliance (initialize, tools/list, tools/call)
|
|
1468
|
+
# - Session management by FastMCP
|
|
1469
|
+
#
|
|
1470
|
+
# Example usage with MCP client:
|
|
1471
|
+
# from mcp import ClientSession
|
|
1472
|
+
# from mcp.client.streamable_http import streamablehttp_client
|
|
1473
|
+
#
|
|
1474
|
+
# async with streamablehttp_client(
|
|
1475
|
+
# "http://localhost:8081/mcp",
|
|
1476
|
+
# headers={"X-Chat-Id": "my-chat-123"}
|
|
1477
|
+
# ) as (read, write, _):
|
|
1478
|
+
# async with ClientSession(read, write) as session:
|
|
1479
|
+
# await session.initialize()
|
|
1480
|
+
# tools = await session.list_tools()
|
|
1481
|
+
# result = await session.call_tool("bash_tool", {
|
|
1482
|
+
# "command": "echo hello",
|
|
1483
|
+
# "description": "Test command"
|
|
1484
|
+
# })
|
|
1485
|
+
|
|
1486
|
+
# Module-level MCP route registration — runs at import time, before lifespan.
|
|
1487
|
+
# ImportError used to be caught here too; removed because the lifespan now
|
|
1488
|
+
# also imports mcp_tools and crashes loud, so a missing module produced two
|
|
1489
|
+
# different failure modes for the same root cause (silent skip here, then
|
|
1490
|
+
# crash from lifespan). Single failure mode is easier to debug.
|
|
1491
|
+
from mcp_tools import get_mcp_app
|
|
1492
|
+
from starlette.routing import Route
|
|
1493
|
+
|
|
1494
|
+
_mcp_asgi_app = get_mcp_app(api_key=MCP_API_KEY)
|
|
1495
|
+
|
|
1496
|
+
|
|
1497
|
+
async def _mcp_endpoint(request):
|
|
1498
|
+
"""Forward to MCP ASGI app."""
|
|
1499
|
+
# Rewrite path to "/" (root of MCP app)
|
|
1500
|
+
scope = dict(request.scope)
|
|
1501
|
+
scope["path"] = "/"
|
|
1502
|
+
scope["raw_path"] = b"/"
|
|
1503
|
+
await _mcp_asgi_app(scope, request.receive, request._send)
|
|
1504
|
+
|
|
1505
|
+
|
|
1506
|
+
app.routes.insert(0, Route("/mcp", endpoint=_mcp_endpoint, methods=["POST", "GET", "DELETE"]))
|
|
1507
|
+
print("[MCP] Native Streamable HTTP MCP app route added at /mcp")
|
|
1508
|
+
|
|
1509
|
+
|
|
1510
|
+
@app.get("/mcp-info", tags=["MCP"], response_model=MCPInfo)
|
|
1511
|
+
async def mcp_info(_auth: str = Depends(verify_mcp_auth)):
|
|
1512
|
+
"""Get MCP endpoint information (for documentation/debugging)."""
|
|
1513
|
+
mcp_server, _ = _init_mcp()
|
|
1514
|
+
|
|
1515
|
+
if mcp_server is None:
|
|
1516
|
+
raise HTTPException(
|
|
1517
|
+
status_code=503,
|
|
1518
|
+
detail="MCP endpoint not available"
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
tools = []
|
|
1522
|
+
for tool in mcp_server._tool_manager.list_tools():
|
|
1523
|
+
tools.append(MCPToolInfo(
|
|
1524
|
+
name=tool.name,
|
|
1525
|
+
description=tool.description.strip() if tool.description else ""
|
|
1526
|
+
))
|
|
1527
|
+
|
|
1528
|
+
return MCPInfo(
|
|
1529
|
+
name="computer-use-mcp",
|
|
1530
|
+
version="1.0.0",
|
|
1531
|
+
description="Computer Use tools via MCP - command execution in isolated Docker containers",
|
|
1532
|
+
tools=tools,
|
|
1533
|
+
headers={
|
|
1534
|
+
"required": ["X-Chat-Id"],
|
|
1535
|
+
"optional": [
|
|
1536
|
+
"X-User-Email",
|
|
1537
|
+
"X-User-Name",
|
|
1538
|
+
"X-Gitlab-Token",
|
|
1539
|
+
"X-Gitlab-Host",
|
|
1540
|
+
"X-Anthropic-Api-Key",
|
|
1541
|
+
"X-Anthropic-Base-Url"
|
|
1542
|
+
]
|
|
1543
|
+
}
|
|
1544
|
+
)
|