@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,1430 @@
|
|
|
1
|
+
# SPDX-License-Identifier: FSL-1.1-Apache-2.0
|
|
2
|
+
# Copyright (c) 2025 Open Computer Use Contributors
|
|
3
|
+
"""
|
|
4
|
+
MCP Server Tools for Computer Use
|
|
5
|
+
|
|
6
|
+
Provides MCP tools (bash, str_replace, create_file, view, sub_agent) via Streamable HTTP.
|
|
7
|
+
Works with local Docker socket for container management.
|
|
8
|
+
|
|
9
|
+
ARCHITECTURE:
|
|
10
|
+
- File-server runs alongside Docker daemon on the same host
|
|
11
|
+
- Docker containers are created/managed via local Docker socket
|
|
12
|
+
- Each chat gets its own isolated container: owui-chat-{chat_id}
|
|
13
|
+
|
|
14
|
+
GITLAB TOKEN FETCHING:
|
|
15
|
+
Priority order for GitLab token:
|
|
16
|
+
1. X-Gitlab-Token header (direct from client)
|
|
17
|
+
2. MCP Tokens Wrapper (fetches token by user email)
|
|
18
|
+
3. No token (continue without GitLab auth)
|
|
19
|
+
|
|
20
|
+
HTTP Headers — all optional except Chat ID. Headers override env var defaults.
|
|
21
|
+
|
|
22
|
+
| Parameter | Header | Alt Header (OpenWebUI) | Required | Fallback |
|
|
23
|
+
|-------------------|------------------------|--------------------------------|----------|---------------------------------|
|
|
24
|
+
| Chat ID | X-Chat-Id | X-OpenWebUI-Chat-Id | Yes | — |
|
|
25
|
+
| User Email | X-User-Email | X-OpenWebUI-User-Email | No | — |
|
|
26
|
+
| User Name | X-User-Name | X-OpenWebUI-User-Name | No | — |
|
|
27
|
+
| GitLab Token | X-Gitlab-Token | X-OpenWebUI-Gitlab-Token | No | MCP Tokens Wrapper by email |
|
|
28
|
+
| GitLab Host | X-Gitlab-Host | X-OpenWebUI-Gitlab-Host | No | gitlab.com |
|
|
29
|
+
| Anthropic API Key | X-Anthropic-Api-Key | X-OpenWebUI-Anthropic-Api-Key | No | ANTHROPIC_AUTH_TOKEN env |
|
|
30
|
+
| Anthropic Base URL| X-Anthropic-Base-Url | X-OpenWebUI-Anthropic-Base-Url | No | ANTHROPIC_BASE_URL env |
|
|
31
|
+
| MCP Tokens URL | X-Mcp-Tokens-Url | X-OpenWebUI-Mcp-Tokens-Url | No | MCP_TOKENS_URL env |
|
|
32
|
+
| MCP Tokens API Key| X-Mcp-Tokens-Api-Key | X-OpenWebUI-Mcp-Tokens-Api-Key | No | MCP_TOKENS_API_KEY env |
|
|
33
|
+
| MCP Servers | X-Mcp-Servers | X-OpenWebUI-Mcp-Servers | No | — |
|
|
34
|
+
|
|
35
|
+
Environment Variables (computer-use-orchestrator defaults):
|
|
36
|
+
- MCP_TOKENS_URL: MCP Tokens Wrapper service (optional, for centralized token management)
|
|
37
|
+
- MCP_TOKENS_API_KEY: Internal API key for MCP Tokens Wrapper
|
|
38
|
+
- ANTHROPIC_AUTH_TOKEN: Shared LiteLLM proxy key for Claude Code sub-agent
|
|
39
|
+
- ANTHROPIC_BASE_URL: LLM API base URL (default: https://api.anthropic.com)
|
|
40
|
+
- CLAUDE_SUB_AGENT_DEFAULT_MODEL: Default model for sub_agent when SUBAGENT_CLI=claude (default: sonnet)
|
|
41
|
+
- OPENCODE_SUB_AGENT_DEFAULT_MODEL: Default model for sub_agent when SUBAGENT_CLI=opencode (no default — required)
|
|
42
|
+
- CODEX_SUB_AGENT_DEFAULT_MODEL: Default model for sub_agent when SUBAGENT_CLI=codex (no default — required)
|
|
43
|
+
- SUB_AGENT_MAX_TURNS: Default max turns for sub_agent (default: 25)
|
|
44
|
+
- SUB_AGENT_TIMEOUT: Timeout for sub_agent execution in seconds (default: 3600)
|
|
45
|
+
|
|
46
|
+
LiteLLM Integration:
|
|
47
|
+
mcp_servers:
|
|
48
|
+
docker_ai:
|
|
49
|
+
url: "http://computer-use-server:8081/mcp"
|
|
50
|
+
transport: "http"
|
|
51
|
+
auth_type: "bearer_token"
|
|
52
|
+
auth_value: "<MCP_API_KEY>"
|
|
53
|
+
extra_headers:
|
|
54
|
+
# OpenWebUI headers (alternative)
|
|
55
|
+
- "x-openwebui-chat-id"
|
|
56
|
+
- "x-openwebui-user-email"
|
|
57
|
+
- "x-openwebui-user-name"
|
|
58
|
+
- "x-openwebui-gitlab-token"
|
|
59
|
+
- "x-openwebui-gitlab-host"
|
|
60
|
+
- "x-openwebui-anthropic-api-key"
|
|
61
|
+
- "x-openwebui-anthropic-base-url"
|
|
62
|
+
# Direct headers
|
|
63
|
+
- "x-chat-id"
|
|
64
|
+
- "x-user-email"
|
|
65
|
+
- "x-user-name"
|
|
66
|
+
- "x-gitlab-token"
|
|
67
|
+
- "x-gitlab-host"
|
|
68
|
+
- "x-anthropic-api-key"
|
|
69
|
+
- "x-anthropic-base-url"
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
import os
|
|
73
|
+
import re
|
|
74
|
+
import json
|
|
75
|
+
import shlex
|
|
76
|
+
import time
|
|
77
|
+
import asyncio
|
|
78
|
+
import urllib.parse
|
|
79
|
+
from typing import Optional, List, Annotated
|
|
80
|
+
|
|
81
|
+
from mcp.server.fastmcp import FastMCP, Context
|
|
82
|
+
from pydantic import Field
|
|
83
|
+
import skill_manager
|
|
84
|
+
from context_vars import (
|
|
85
|
+
current_chat_id, current_user_email, current_user_name,
|
|
86
|
+
current_gitlab_token, current_gitlab_host,
|
|
87
|
+
current_anthropic_auth_token, current_anthropic_base_url,
|
|
88
|
+
current_mcp_tokens_url, current_mcp_tokens_api_key, current_mcp_servers,
|
|
89
|
+
current_instructions,
|
|
90
|
+
)
|
|
91
|
+
from system_prompt import render_system_prompt
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Single-user mode: "" (lenient default), "true" (solo), "false" (strict multi-user)
|
|
95
|
+
SINGLE_USER_MODE = os.getenv("SINGLE_USER_MODE", "").lower()
|
|
96
|
+
|
|
97
|
+
# Warning appended to tool responses when using default container in lenient mode
|
|
98
|
+
DEFAULT_CHAT_ID_WARNING = (
|
|
99
|
+
"\n\n---\n"
|
|
100
|
+
"Note: No X-Chat-Id header provided — using shared 'default' container.\n"
|
|
101
|
+
"All sessions without a chat ID share the same container (files, processes, state).\n\n"
|
|
102
|
+
"Options:\n"
|
|
103
|
+
"- Set SINGLE_USER_MODE=true in .env to always use one container (single-user setup)\n"
|
|
104
|
+
"- Set SINGLE_USER_MODE=false to require X-Chat-Id (multi-user setup)\n"
|
|
105
|
+
"- Pass X-Chat-Id header in your MCP client for per-session isolation\n"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Error returned when chat_id is missing in strict multi-user mode
|
|
109
|
+
CHAT_ID_REQUIRED_ERROR = (
|
|
110
|
+
"Error: X-Chat-Id header is required (SINGLE_USER_MODE=false).\n\n"
|
|
111
|
+
"In multi-user mode, every request must include X-Chat-Id for container isolation.\n"
|
|
112
|
+
"Pass -H \"X-Chat-Id: your-unique-id\" or set SINGLE_USER_MODE=true for single-user setup."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _validate_chat_id() -> tuple[str, str | None]:
|
|
117
|
+
"""
|
|
118
|
+
Validate chat_id based on SINGLE_USER_MODE setting.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
tuple: (chat_id, error_message) - error_message is None if valid
|
|
122
|
+
"""
|
|
123
|
+
chat_id = current_chat_id.get()
|
|
124
|
+
|
|
125
|
+
if SINGLE_USER_MODE == "true":
|
|
126
|
+
return "default", None
|
|
127
|
+
|
|
128
|
+
if chat_id == "default":
|
|
129
|
+
if SINGLE_USER_MODE == "false":
|
|
130
|
+
return chat_id, CHAT_ID_REQUIRED_ERROR
|
|
131
|
+
return chat_id, None
|
|
132
|
+
|
|
133
|
+
return chat_id, None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _get_default_chat_warning() -> str:
|
|
137
|
+
"""Return warning suffix if using default chat_id in lenient mode."""
|
|
138
|
+
if SINGLE_USER_MODE in ("true", "false"):
|
|
139
|
+
return ""
|
|
140
|
+
if current_chat_id.get() == "default":
|
|
141
|
+
print("[WARN] No X-Chat-Id header and SINGLE_USER_MODE not set — using shared 'default' container")
|
|
142
|
+
return DEFAULT_CHAT_ID_WARNING
|
|
143
|
+
return ""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Configuration from environment
|
|
147
|
+
|
|
148
|
+
# Docker management extracted to docker_manager.py
|
|
149
|
+
from docker_manager import (
|
|
150
|
+
get_docker_client, get_container_cdp_address,
|
|
151
|
+
_get_or_create_container, _execute_bash, execute_bash_streaming, _execute_python_with_stdin,
|
|
152
|
+
_reset_shutdown_timer, _get_compose_network_name,
|
|
153
|
+
build_mcp_config, build_mcp_config_write_script,
|
|
154
|
+
_fetch_gitlab_token, _ensure_gitlab_token,
|
|
155
|
+
DOCKER_SOCKET, DOCKER_IMAGE, CONTAINER_MEM_LIMIT, CONTAINER_CPU_LIMIT,
|
|
156
|
+
COMMAND_TIMEOUT, ENABLE_NETWORK, USER_DATA_BASE_PATH, PUBLIC_BASE_URL,
|
|
157
|
+
MCP_TOKENS_URL, MCP_TOKENS_API_KEY,
|
|
158
|
+
SUB_AGENT_MAX_TURNS, SUB_AGENT_TIMEOUT,
|
|
159
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL,
|
|
160
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL,
|
|
161
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL,
|
|
162
|
+
)
|
|
163
|
+
from cli_runtime import dispatch as cli_dispatch, Cli, resolve_cli, resolve_subagent_model
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ============================================================================
|
|
168
|
+
# Progress Utilities (moved from computer_use_tools.py for server-side use)
|
|
169
|
+
# ============================================================================
|
|
170
|
+
|
|
171
|
+
def format_elapsed_time(seconds: int) -> str:
|
|
172
|
+
"""Format elapsed time as human-readable string (e.g., '45s', '2m 15s')."""
|
|
173
|
+
if seconds < 60:
|
|
174
|
+
return f"{seconds}s"
|
|
175
|
+
minutes = seconds // 60
|
|
176
|
+
remaining_seconds = seconds % 60
|
|
177
|
+
if remaining_seconds == 0:
|
|
178
|
+
return f"{minutes}m"
|
|
179
|
+
return f"{minutes}m {remaining_seconds}s"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
_TOOL_LABELS = {
|
|
183
|
+
"Bash": "Command",
|
|
184
|
+
"Read": "Reading",
|
|
185
|
+
"Write": "Writing",
|
|
186
|
+
"Edit": "Editing",
|
|
187
|
+
"Grep": "Searching",
|
|
188
|
+
"Glob": "Finding files",
|
|
189
|
+
"WebSearch": "Web search",
|
|
190
|
+
"WebFetch": "Loading page",
|
|
191
|
+
"TodoWrite": "Tasks",
|
|
192
|
+
"Agent": "Subtask",
|
|
193
|
+
"ToolSearch": "Selecting tool",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def parse_last_action(lines: list) -> Optional[str]:
|
|
198
|
+
"""
|
|
199
|
+
Parse JSONL lines from Claude session log and return last meaningful action.
|
|
200
|
+
Returns whichever came last in the log (text or tool_use) to avoid showing
|
|
201
|
+
stale text while a long tool is executing.
|
|
202
|
+
"""
|
|
203
|
+
last_action = None
|
|
204
|
+
|
|
205
|
+
for line in lines:
|
|
206
|
+
if not line.strip():
|
|
207
|
+
continue
|
|
208
|
+
try:
|
|
209
|
+
data = json.loads(line)
|
|
210
|
+
if data.get("type") == "assistant":
|
|
211
|
+
content = data.get("message", {}).get("content", [])
|
|
212
|
+
for item in content:
|
|
213
|
+
if item.get("type") == "text":
|
|
214
|
+
text = item.get("text", "")[:80]
|
|
215
|
+
text = text.replace('\n', ' ').strip()
|
|
216
|
+
if text:
|
|
217
|
+
last_action = text
|
|
218
|
+
elif item.get("type") == "tool_use":
|
|
219
|
+
name = item.get("name", "unknown")
|
|
220
|
+
inp = item.get("input", {})
|
|
221
|
+
detail = get_tool_detail(name, inp)
|
|
222
|
+
tool_label = _TOOL_LABELS.get(name, name)
|
|
223
|
+
if detail:
|
|
224
|
+
last_action = f"{tool_label}: {detail}"
|
|
225
|
+
else:
|
|
226
|
+
last_action = f"{tool_label}..."
|
|
227
|
+
except (json.JSONDecodeError, KeyError, TypeError):
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
return last_action
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_tool_detail(name: str, inp: dict) -> Optional[str]:
|
|
234
|
+
"""Extract useful detail from tool input for status display."""
|
|
235
|
+
try:
|
|
236
|
+
# First check for description field (Claude fills this for our tools)
|
|
237
|
+
desc = inp.get("description", "")
|
|
238
|
+
if desc:
|
|
239
|
+
return desc.replace('\n', ' ').strip()
|
|
240
|
+
|
|
241
|
+
# Fallback to tool-specific extraction
|
|
242
|
+
if name == "Bash":
|
|
243
|
+
cmd = inp.get("command", "")
|
|
244
|
+
return cmd.replace('\n', ' ').strip() if cmd else None
|
|
245
|
+
elif name == "WebSearch":
|
|
246
|
+
return inp.get("query", "")
|
|
247
|
+
elif name == "Write":
|
|
248
|
+
path = inp.get("file_path", "")
|
|
249
|
+
return path.split("/")[-1] if path else None
|
|
250
|
+
elif name == "Read":
|
|
251
|
+
path = inp.get("file_path", "")
|
|
252
|
+
return path.split("/")[-1] if path else None
|
|
253
|
+
elif name == "Edit":
|
|
254
|
+
path = inp.get("file_path", "")
|
|
255
|
+
return path.split("/")[-1] if path else None
|
|
256
|
+
elif name == "Grep":
|
|
257
|
+
pattern = inp.get("pattern", "")
|
|
258
|
+
return f'"{pattern}"' if pattern else None
|
|
259
|
+
elif name == "Glob":
|
|
260
|
+
return inp.get("pattern", "")
|
|
261
|
+
elif name == "TodoWrite":
|
|
262
|
+
todos = inp.get("todos", [])
|
|
263
|
+
in_progress = [t for t in todos if t.get("status") == "in_progress"]
|
|
264
|
+
if in_progress:
|
|
265
|
+
return in_progress[0].get("content", "")
|
|
266
|
+
return f"{len(todos)} tasks"
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ============================================================================
|
|
274
|
+
# MCP Server Definition
|
|
275
|
+
# ============================================================================
|
|
276
|
+
|
|
277
|
+
# Static instructions kwarg — fallback when Tier 4's dynamic override is
|
|
278
|
+
# bypassed (client that ignores InitializeResult.instructions, or the
|
|
279
|
+
# render_system_prompt pre-render failed). Points at the other tiers so any
|
|
280
|
+
# client hitting this baseline learns where to fetch the real content.
|
|
281
|
+
_STATIC_INSTRUCTIONS = (
|
|
282
|
+
"Computer Use tools: bash, file edits, browser, sub-agent — in an isolated "
|
|
283
|
+
"Docker sandbox. Full per-session guide is at /home/assistant/README.md "
|
|
284
|
+
"(call the view tool to read it). Uploaded files are exposed via "
|
|
285
|
+
"resources/list."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
mcp = FastMCP(
|
|
289
|
+
name="computer-use-mcp",
|
|
290
|
+
instructions=_STATIC_INSTRUCTIONS,
|
|
291
|
+
streamable_http_path="/", # Root path — mounted at /mcp in FastAPI
|
|
292
|
+
stateless_http=True, # Each request is independent (no session persistence)
|
|
293
|
+
transport_security={ # Behind proxy (LiteLLM/nginx), any Host is valid
|
|
294
|
+
"enable_dns_rebinding_protection": False,
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ============================================================================
|
|
300
|
+
# Tier 4 — Dynamic InitializeResult.instructions
|
|
301
|
+
# ============================================================================
|
|
302
|
+
#
|
|
303
|
+
# The static `instructions=` kwarg above ships as a constant in every
|
|
304
|
+
# InitializeResult. We want per-chat content (file URLs, skills) to ride in
|
|
305
|
+
# that same field so clients like Claude Desktop / MCP Inspector (which
|
|
306
|
+
# render `instructions` directly) get dynamic content without any explicit
|
|
307
|
+
# prompts/get call.
|
|
308
|
+
#
|
|
309
|
+
# Mechanism (works ONLY because stateless_http=True):
|
|
310
|
+
# 1. Middleware awaits render_system_prompt(chat_id, user_email) BEFORE
|
|
311
|
+
# dispatching the MCP handler and stores the result in
|
|
312
|
+
# `current_instructions` ContextVar (see MCPContextMiddleware below).
|
|
313
|
+
# 2. `streamable_http_manager._handle_stateless_request` (verified at
|
|
314
|
+
# .venv/.../mcp/server/streamable_http_manager.py:196) spins up a fresh
|
|
315
|
+
# `server.run(..., initialization_options, stateless=True)` per HTTP
|
|
316
|
+
# request, and `create_initialization_options()` is called INSIDE that
|
|
317
|
+
# per-request task — after the middleware has run.
|
|
318
|
+
# 3. `lowlevel/server.py:188` reads `self.instructions` at that moment.
|
|
319
|
+
# We override the property to return the ContextVar value.
|
|
320
|
+
# 4. `session.py:183` echoes it into `InitializeResult.instructions`.
|
|
321
|
+
#
|
|
322
|
+
# Stateful mode would break this: a long-lived session caches init_options at
|
|
323
|
+
# construction time. Do NOT flip stateless_http=False without re-reading the
|
|
324
|
+
# SDK source above.
|
|
325
|
+
#
|
|
326
|
+
# Private-API caveat: we swap `mcp._mcp_server.__class__` in place. FastMCP
|
|
327
|
+
# doesn't expose a public hook. Pin the `mcp` version in requirements.txt to
|
|
328
|
+
# protect against attribute renames.
|
|
329
|
+
from mcp.server.lowlevel.server import Server as _LowlevelServer
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class _DynamicInstructionsServer(_LowlevelServer):
|
|
333
|
+
"""Subclass that reads `instructions` from the current-request ContextVar.
|
|
334
|
+
|
|
335
|
+
Falls back to the static string if the middleware hasn't pre-rendered
|
|
336
|
+
(e.g. a render exception, or a direct in-process call without ASGI)."""
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def instructions(self): # type: ignore[override]
|
|
340
|
+
return current_instructions.get() or self._static_instructions
|
|
341
|
+
|
|
342
|
+
@instructions.setter
|
|
343
|
+
def instructions(self, value):
|
|
344
|
+
self._static_instructions = value
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# Rebind class on the already-constructed lowlevel Server so the property
|
|
348
|
+
# override takes effect without reconstructing FastMCP. The base class stores
|
|
349
|
+
# `instructions` in `self.__dict__`; move it to the `_static_instructions`
|
|
350
|
+
# slot before swapping the class so the property getter can read it as the
|
|
351
|
+
# fallback.
|
|
352
|
+
#
|
|
353
|
+
# Defensive shape assertions — these guard against silent breakage when the
|
|
354
|
+
# `mcp` SDK changes the private attribute layout (e.g. moves `_mcp_server` to
|
|
355
|
+
# `_lowlevel_server`, or switches to __slots__). Without them, an SDK rename
|
|
356
|
+
# would silently drop us back to static instructions for every chat — Tier 4
|
|
357
|
+
# would just stop working with no error to debug.
|
|
358
|
+
assert hasattr(mcp, "_mcp_server"), (
|
|
359
|
+
"FastMCP no longer exposes _mcp_server — Tier 4 dynamic instructions "
|
|
360
|
+
"broke. Re-pin mcp in requirements.txt and update mcp_tools.py."
|
|
361
|
+
)
|
|
362
|
+
_existing_lowlevel_server = mcp._mcp_server # private; pinned mcp version guards
|
|
363
|
+
assert isinstance(_existing_lowlevel_server, _LowlevelServer), (
|
|
364
|
+
f"mcp._mcp_server is not a lowlevel Server (got {type(_existing_lowlevel_server)!r}). "
|
|
365
|
+
"Tier 4 class-swap will not work. Re-pin mcp."
|
|
366
|
+
)
|
|
367
|
+
assert hasattr(_existing_lowlevel_server, "__dict__"), (
|
|
368
|
+
"Lowlevel Server uses __slots__ — class-swap pop() will fail. Re-pin mcp."
|
|
369
|
+
)
|
|
370
|
+
_existing_instructions_value = _existing_lowlevel_server.__dict__.pop(
|
|
371
|
+
"instructions", _STATIC_INSTRUCTIONS
|
|
372
|
+
)
|
|
373
|
+
_existing_lowlevel_server._static_instructions = _existing_instructions_value
|
|
374
|
+
_existing_lowlevel_server.__class__ = _DynamicInstructionsServer
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
async def send_progress(ctx: "Context", progress: float, total: float, message: str):
|
|
378
|
+
"""Send progress notification with related_request_id for stateless HTTP mode.
|
|
379
|
+
|
|
380
|
+
Workaround for MCP SDK bug: ctx.report_progress() doesn't pass
|
|
381
|
+
related_request_id, so notifications get lost in stateless_http mode
|
|
382
|
+
(routed to non-existent GET SSE stream instead of request stream).
|
|
383
|
+
"""
|
|
384
|
+
rc = ctx.request_context
|
|
385
|
+
if not rc or not rc.meta or not rc.meta.progressToken:
|
|
386
|
+
return
|
|
387
|
+
await rc.session.send_progress_notification(
|
|
388
|
+
progress_token=rc.meta.progressToken,
|
|
389
|
+
progress=progress,
|
|
390
|
+
total=total,
|
|
391
|
+
message=message,
|
|
392
|
+
related_request_id=str(rc.request_id),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Custom type for view_range
|
|
397
|
+
ViewRange = Annotated[
|
|
398
|
+
Optional[List[int]],
|
|
399
|
+
Field(
|
|
400
|
+
default=None,
|
|
401
|
+
min_length=2,
|
|
402
|
+
max_length=2,
|
|
403
|
+
description="Optional line range [start_line, end_line]. Use [start, -1] to view from start to end."
|
|
404
|
+
)
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Output truncation & command semantics (inspired by Claude Code BashTool)
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
MAX_BASH_OUTPUT_CHARS = 30_000
|
|
413
|
+
|
|
414
|
+
# Commands where exit code 1 is NOT an error (semantic exit codes)
|
|
415
|
+
# grep/rg: 1=no matches, 2+=error
|
|
416
|
+
# find: 1=partial access, 2+=error
|
|
417
|
+
# diff: 1=files differ, 2+=error
|
|
418
|
+
# test/[: 1=condition false, 2+=error
|
|
419
|
+
COMMAND_SEMANTICS = {
|
|
420
|
+
'grep': {'threshold': 2, 'message': 'No matches found'},
|
|
421
|
+
'rg': {'threshold': 2, 'message': 'No matches found'},
|
|
422
|
+
'find': {'threshold': 2, 'message': 'Some directories were inaccessible'},
|
|
423
|
+
'diff': {'threshold': 2, 'message': 'Files differ'},
|
|
424
|
+
'test': {'threshold': 2, 'message': 'Condition is false'},
|
|
425
|
+
'[': {'threshold': 2, 'message': 'Condition is false'},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _get_first_command(command: str) -> str:
|
|
430
|
+
"""Extract the first command name from a shell command string."""
|
|
431
|
+
cmd = command.strip()
|
|
432
|
+
# Skip env vars like VAR=val, sudo, etc.
|
|
433
|
+
for token in cmd.split():
|
|
434
|
+
if '=' in token:
|
|
435
|
+
continue
|
|
436
|
+
if token in ('sudo', 'env', 'nice', 'time', 'strace'):
|
|
437
|
+
continue
|
|
438
|
+
# Return basename (e.g. /usr/bin/grep -> grep)
|
|
439
|
+
return token.rsplit('/', 1)[-1]
|
|
440
|
+
return ''
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _apply_command_semantics(command: str, exit_code: int, output: str) -> str:
|
|
444
|
+
"""Apply command-specific exit code interpretation."""
|
|
445
|
+
if exit_code == 0:
|
|
446
|
+
return output if output else "[No output]"
|
|
447
|
+
|
|
448
|
+
first_cmd = _get_first_command(command)
|
|
449
|
+
semantic = COMMAND_SEMANTICS.get(first_cmd)
|
|
450
|
+
|
|
451
|
+
if semantic and exit_code < semantic['threshold']:
|
|
452
|
+
# Exit code is informational, not an error
|
|
453
|
+
return output if output else semantic['message']
|
|
454
|
+
|
|
455
|
+
# Default: return output or exit code
|
|
456
|
+
return output if output else f"[Exit code: {exit_code}]"
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _truncate_output(output: str, max_chars: int = MAX_BASH_OUTPUT_CHARS) -> str:
|
|
460
|
+
"""Truncate large output, keeping head and tail."""
|
|
461
|
+
if len(output) <= max_chars:
|
|
462
|
+
return output
|
|
463
|
+
half = max_chars // 2
|
|
464
|
+
total = len(output)
|
|
465
|
+
return (
|
|
466
|
+
output[:half]
|
|
467
|
+
+ f"\n\n... [Output truncated: {total} chars total, showing first and last {half} chars.\n"
|
|
468
|
+
+ f"Use head/tail/view to read specific parts] ...\n\n"
|
|
469
|
+
+ output[-half:]
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@mcp.tool()
|
|
474
|
+
async def bash_tool(command: str, description: str, ctx: Context) -> str:
|
|
475
|
+
"""
|
|
476
|
+
Run a bash command in the container.
|
|
477
|
+
|
|
478
|
+
If you've lost track of your environment (chat_id, file URLs, available
|
|
479
|
+
skills), re-read /home/assistant/README.md.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
command: Bash command to run in container
|
|
483
|
+
description: Why I'm running this command
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Command output (stdout/stderr)
|
|
487
|
+
"""
|
|
488
|
+
chat_id, error = _validate_chat_id()
|
|
489
|
+
if error:
|
|
490
|
+
return error
|
|
491
|
+
|
|
492
|
+
try:
|
|
493
|
+
await _ensure_gitlab_token()
|
|
494
|
+
|
|
495
|
+
timeout = int(os.getenv("COMMAND_TIMEOUT", "120"))
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
container = await asyncio.wait_for(
|
|
499
|
+
asyncio.to_thread(_get_or_create_container, chat_id),
|
|
500
|
+
timeout=60,
|
|
501
|
+
)
|
|
502
|
+
except asyncio.TimeoutError:
|
|
503
|
+
return "Error: Container creation timed out (60s). Docker may be overloaded."
|
|
504
|
+
|
|
505
|
+
# Report progress during execution
|
|
506
|
+
start_time = time.time()
|
|
507
|
+
last_output_line: list[str] = [""]
|
|
508
|
+
|
|
509
|
+
def _on_output_line(line: str) -> None:
|
|
510
|
+
last_output_line[0] = line
|
|
511
|
+
|
|
512
|
+
async def _progress_heartbeat():
|
|
513
|
+
while True:
|
|
514
|
+
await asyncio.sleep(15)
|
|
515
|
+
elapsed = int(time.time() - start_time)
|
|
516
|
+
msg = f"Running: {description} ({format_elapsed_time(elapsed)})"
|
|
517
|
+
last = last_output_line[0]
|
|
518
|
+
if last:
|
|
519
|
+
msg += f"\n→ {last}"
|
|
520
|
+
await send_progress(ctx, elapsed, timeout, msg)
|
|
521
|
+
|
|
522
|
+
heartbeat = asyncio.create_task(_progress_heartbeat())
|
|
523
|
+
try:
|
|
524
|
+
result = await asyncio.to_thread(
|
|
525
|
+
execute_bash_streaming, container, command, timeout, _on_output_line
|
|
526
|
+
)
|
|
527
|
+
finally:
|
|
528
|
+
heartbeat.cancel()
|
|
529
|
+
try:
|
|
530
|
+
await heartbeat
|
|
531
|
+
except asyncio.CancelledError:
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
output = _apply_command_semantics(command, result["exit_code"], result["output"])
|
|
535
|
+
return _truncate_output(output) + _get_default_chat_warning()
|
|
536
|
+
except Exception as e:
|
|
537
|
+
return f"Error: {str(e)}"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@mcp.tool()
|
|
541
|
+
async def str_replace(
|
|
542
|
+
description: str,
|
|
543
|
+
old_str: str,
|
|
544
|
+
path: str,
|
|
545
|
+
new_str: str = "",
|
|
546
|
+
ctx: Context = None, # injected by FastMCP; None when called directly
|
|
547
|
+
) -> str:
|
|
548
|
+
"""
|
|
549
|
+
Replace a unique string in a file with another string.
|
|
550
|
+
The string to replace must appear exactly once in the file.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
description: Why I'm making this edit
|
|
554
|
+
old_str: String to replace (must be unique in file)
|
|
555
|
+
path: Path to the file to edit
|
|
556
|
+
new_str: String to replace with (empty to delete)
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Success message or error
|
|
560
|
+
"""
|
|
561
|
+
chat_id, error = _validate_chat_id()
|
|
562
|
+
if error:
|
|
563
|
+
return error
|
|
564
|
+
|
|
565
|
+
if old_str == new_str:
|
|
566
|
+
return "Error: old_str and new_str are identical. No changes would be made."
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
await _ensure_gitlab_token()
|
|
570
|
+
try:
|
|
571
|
+
container = await asyncio.wait_for(
|
|
572
|
+
asyncio.to_thread(_get_or_create_container, chat_id), timeout=60
|
|
573
|
+
)
|
|
574
|
+
except asyncio.TimeoutError:
|
|
575
|
+
return "Error: Container creation timed out (60s)."
|
|
576
|
+
|
|
577
|
+
script = """
|
|
578
|
+
import sys
|
|
579
|
+
import json
|
|
580
|
+
|
|
581
|
+
try:
|
|
582
|
+
data = json.loads(sys.stdin.read())
|
|
583
|
+
path = data['path']
|
|
584
|
+
old_str = data['old_str']
|
|
585
|
+
new_str = data['new_str']
|
|
586
|
+
|
|
587
|
+
with open(path, 'r') as f:
|
|
588
|
+
content = f.read()
|
|
589
|
+
|
|
590
|
+
if old_str not in content:
|
|
591
|
+
print(f"Error: old_str not found in {path}")
|
|
592
|
+
sys.exit(1)
|
|
593
|
+
|
|
594
|
+
count = content.count(old_str)
|
|
595
|
+
if count > 1:
|
|
596
|
+
print(f"Error: Found {count} occurrences of old_str in {path}. Add more surrounding context to make it unique.")
|
|
597
|
+
sys.exit(1)
|
|
598
|
+
|
|
599
|
+
new_content = content.replace(old_str, new_str, 1)
|
|
600
|
+
|
|
601
|
+
with open(path, 'w') as f:
|
|
602
|
+
f.write(new_content)
|
|
603
|
+
|
|
604
|
+
print(f"Successfully replaced text in {path}")
|
|
605
|
+
except Exception as e:
|
|
606
|
+
print(f"Error: {e}")
|
|
607
|
+
sys.exit(1)
|
|
608
|
+
"""
|
|
609
|
+
payload = json.dumps({"path": path, "old_str": old_str, "new_str": new_str})
|
|
610
|
+
result = await asyncio.to_thread(_execute_python_with_stdin, container, script, payload)
|
|
611
|
+
return result["output"] + _get_default_chat_warning()
|
|
612
|
+
|
|
613
|
+
except Exception as e:
|
|
614
|
+
return f"Error: {str(e)}"
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@mcp.tool()
|
|
618
|
+
async def create_file(
|
|
619
|
+
description: str,
|
|
620
|
+
file_text: str,
|
|
621
|
+
path: str,
|
|
622
|
+
ctx: Context = None,
|
|
623
|
+
) -> str:
|
|
624
|
+
"""
|
|
625
|
+
Create a new file with content in the container.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
description: Why I'm creating this file. ALWAYS PROVIDE THIS PARAMETER FIRST.
|
|
629
|
+
file_text: Content to write to the file. ALWAYS PROVIDE THIS PARAMETER SECOND.
|
|
630
|
+
path: Path to the file to create. ALWAYS PROVIDE THIS PARAMETER LAST.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
Success message or error
|
|
634
|
+
"""
|
|
635
|
+
chat_id, error = _validate_chat_id()
|
|
636
|
+
if error:
|
|
637
|
+
return error
|
|
638
|
+
|
|
639
|
+
try:
|
|
640
|
+
await _ensure_gitlab_token()
|
|
641
|
+
try:
|
|
642
|
+
container = await asyncio.wait_for(
|
|
643
|
+
asyncio.to_thread(_get_or_create_container, chat_id), timeout=60
|
|
644
|
+
)
|
|
645
|
+
except asyncio.TimeoutError:
|
|
646
|
+
return "Error: Container creation timed out (60s)."
|
|
647
|
+
|
|
648
|
+
script = """
|
|
649
|
+
import sys
|
|
650
|
+
import json
|
|
651
|
+
import os
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
data = json.loads(sys.stdin.read())
|
|
655
|
+
path = data['path']
|
|
656
|
+
file_text = data['file_text']
|
|
657
|
+
|
|
658
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
659
|
+
|
|
660
|
+
with open(path, 'w') as f:
|
|
661
|
+
f.write(file_text)
|
|
662
|
+
|
|
663
|
+
print(f"Successfully created {path}")
|
|
664
|
+
except Exception as e:
|
|
665
|
+
print(f"Error: {e}")
|
|
666
|
+
sys.exit(1)
|
|
667
|
+
"""
|
|
668
|
+
payload = json.dumps({"path": path, "file_text": file_text})
|
|
669
|
+
result = await asyncio.to_thread(_execute_python_with_stdin, container, script, payload)
|
|
670
|
+
output = result["output"] if result["success"] else f"Error: {result['output']}"
|
|
671
|
+
return output + _get_default_chat_warning()
|
|
672
|
+
|
|
673
|
+
except Exception as e:
|
|
674
|
+
return f"Error: {str(e)}"
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
@mcp.tool()
|
|
678
|
+
async def view(
|
|
679
|
+
description: str,
|
|
680
|
+
path: str,
|
|
681
|
+
view_range: Optional[List[int]] = None,
|
|
682
|
+
ctx: Context = None,
|
|
683
|
+
) -> str:
|
|
684
|
+
"""
|
|
685
|
+
View text files or directory listings.
|
|
686
|
+
Binary files are detected and rejected with instructions to read SKILL documentation.
|
|
687
|
+
|
|
688
|
+
If you've lost track of your environment (chat_id, file URLs, available
|
|
689
|
+
skills), re-read /home/assistant/README.md.
|
|
690
|
+
|
|
691
|
+
Supported path types:
|
|
692
|
+
- Directories: Lists files and directories with details
|
|
693
|
+
- Text files: Displays numbered lines. You can optionally specify a view_range.
|
|
694
|
+
- Binary files (.xlsx, .docx, .pptx, .pdf, etc.): Returns error with SKILL.md instructions
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
description: Why I need to view this
|
|
698
|
+
path: Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`
|
|
699
|
+
view_range: Optional line range [start_line, end_line]. Use [start, -1] to view from start to end.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
File contents, directory listing, or error message
|
|
703
|
+
"""
|
|
704
|
+
chat_id, error = _validate_chat_id()
|
|
705
|
+
if error:
|
|
706
|
+
return error
|
|
707
|
+
|
|
708
|
+
try:
|
|
709
|
+
await _ensure_gitlab_token()
|
|
710
|
+
try:
|
|
711
|
+
container = await asyncio.wait_for(
|
|
712
|
+
asyncio.to_thread(_get_or_create_container, chat_id), timeout=60
|
|
713
|
+
)
|
|
714
|
+
except asyncio.TimeoutError:
|
|
715
|
+
return "Error: Container creation timed out (60s)."
|
|
716
|
+
|
|
717
|
+
quoted_path = shlex.quote(path)
|
|
718
|
+
|
|
719
|
+
# Image extensions — handled separately (resize+return as image content)
|
|
720
|
+
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
|
|
721
|
+
|
|
722
|
+
# Binary file hints (non-image)
|
|
723
|
+
binary_file_hints = {
|
|
724
|
+
'.xlsx': 'Excel spreadsheet. Read SKILL first:\n view /mnt/skills/public/xlsx/SKILL.md',
|
|
725
|
+
'.xls': 'Excel spreadsheet (old). Read SKILL first:\n view /mnt/skills/public/xlsx/SKILL.md',
|
|
726
|
+
'.docx': 'Word document. Read SKILL first:\n view /mnt/skills/public/docx/SKILL.md',
|
|
727
|
+
'.pptx': 'PowerPoint. Read SKILL first:\n view /mnt/skills/public/pptx/SKILL.md',
|
|
728
|
+
'.pdf': 'PDF document. Read SKILL first:\n view /mnt/skills/public/pdf/SKILL.md',
|
|
729
|
+
'.zip': 'ZIP archive. Use: unzip -l {path}',
|
|
730
|
+
'.tar': 'TAR archive. Use: tar -tvf {path}',
|
|
731
|
+
'.gz': 'Gzip file. Use: gunzip -c {path} | head -n 100',
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
file_ext = None
|
|
735
|
+
path_lower = path.lower()
|
|
736
|
+
for ext in list(binary_file_hints.keys()) + list(image_extensions):
|
|
737
|
+
if path_lower.endswith(ext):
|
|
738
|
+
file_ext = ext
|
|
739
|
+
break
|
|
740
|
+
|
|
741
|
+
if file_ext and file_ext in image_extensions:
|
|
742
|
+
# Image file — resize+compress in container, return as structured content
|
|
743
|
+
try:
|
|
744
|
+
py_code = (
|
|
745
|
+
"from PIL import Image; from io import BytesIO; import base64,sys; "
|
|
746
|
+
f"img=Image.open({path!r}); "
|
|
747
|
+
"mx=1280; "
|
|
748
|
+
"img.thumbnail((mx,mx),Image.Resampling.LANCZOS) if max(img.size)>mx else None; "
|
|
749
|
+
"img=img.convert('RGB') if img.mode in ('RGBA','P') else img; "
|
|
750
|
+
"b=BytesIO(); img.save(b,format='JPEG',quality=80); "
|
|
751
|
+
"sys.stdout.write(base64.b64encode(b.getvalue()).decode())"
|
|
752
|
+
)
|
|
753
|
+
resize_cmd = f"python3 -c {shlex.quote(py_code)}"
|
|
754
|
+
b64_result = await asyncio.to_thread(_execute_bash, container, resize_cmd)
|
|
755
|
+
if b64_result["exit_code"] != 0:
|
|
756
|
+
return f"Error viewing image {path}: {b64_result['output']}"
|
|
757
|
+
image_b64 = b64_result["output"].strip()
|
|
758
|
+
return [
|
|
759
|
+
{"type": "text", "text": f"Image: {path}"},
|
|
760
|
+
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}}
|
|
761
|
+
]
|
|
762
|
+
except Exception as e:
|
|
763
|
+
return f"Error processing image {path}: {e}"
|
|
764
|
+
|
|
765
|
+
elif file_ext and file_ext in binary_file_hints:
|
|
766
|
+
hint = binary_file_hints[file_ext].format(path=path)
|
|
767
|
+
command = f"""
|
|
768
|
+
if [ -f {quoted_path} ]; then
|
|
769
|
+
echo "Error: Cannot view binary file with 'cat'. This is a {file_ext} file."
|
|
770
|
+
echo ""
|
|
771
|
+
echo "{hint}"
|
|
772
|
+
exit 1
|
|
773
|
+
elif [ -d {quoted_path} ]; then
|
|
774
|
+
ls -lah {quoted_path}
|
|
775
|
+
else
|
|
776
|
+
echo "Error: path not found"
|
|
777
|
+
exit 1
|
|
778
|
+
fi
|
|
779
|
+
"""
|
|
780
|
+
else:
|
|
781
|
+
if view_range:
|
|
782
|
+
start, end = view_range
|
|
783
|
+
if end == -1:
|
|
784
|
+
cat_command = f"sed -n '{start},$p' {quoted_path} | cat -n"
|
|
785
|
+
else:
|
|
786
|
+
cat_command = f"sed -n '{start},{end}p' {quoted_path} | cat -n"
|
|
787
|
+
else:
|
|
788
|
+
cat_command = f"cat -n {quoted_path}"
|
|
789
|
+
|
|
790
|
+
command = f"""
|
|
791
|
+
if [ -f {quoted_path} ]; then
|
|
792
|
+
{cat_command}
|
|
793
|
+
elif [ -d {quoted_path} ]; then
|
|
794
|
+
ls -lah {quoted_path}
|
|
795
|
+
else
|
|
796
|
+
echo "Error: path not found"
|
|
797
|
+
exit 1
|
|
798
|
+
fi
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
result = await asyncio.to_thread(_execute_bash, container, command)
|
|
802
|
+
output = result["output"] if result["output"] else "Error: No output"
|
|
803
|
+
|
|
804
|
+
# Truncate if needed (30K limit, matching bash_tool MAX_BASH_OUTPUT_CHARS)
|
|
805
|
+
if not view_range and len(output) > 30000:
|
|
806
|
+
truncation_msg = f"\n\n... [File truncated - middle omitted. Total: {len(output)} chars. Use view_range.] ...\n\n"
|
|
807
|
+
output = output[:15000] + truncation_msg + output[-15000:]
|
|
808
|
+
|
|
809
|
+
return output + _get_default_chat_warning()
|
|
810
|
+
|
|
811
|
+
except Exception as e:
|
|
812
|
+
return f"Error: {str(e)}"
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# ---------------------------------------------------------------------------
|
|
816
|
+
# Per-CLI docstring variants for the sub_agent tool.
|
|
817
|
+
# FastMCP captures fn.__doc__ at mcp.add_tool() registration time, so we
|
|
818
|
+
# assign the correct variant before calling mcp.add_tool(sub_agent).
|
|
819
|
+
# See RESEARCH.md "FastMCP Docstring Binding Mechanics" (base.py:66).
|
|
820
|
+
# ---------------------------------------------------------------------------
|
|
821
|
+
|
|
822
|
+
_SUBAGENT_DOC_CLAUDE = """Spawn a sub-agent (claude code) to perform a focused subtask.
|
|
823
|
+
|
|
824
|
+
COSTLY: Spawns a separate Claude CLI session with its own API budget.
|
|
825
|
+
Use ONLY as a last resort for complex CODE tasks requiring 10+ iterative tool calls.
|
|
826
|
+
|
|
827
|
+
Justified uses:
|
|
828
|
+
- Multi-file refactoring (5+ files) with test verification loops
|
|
829
|
+
- Complex code review with automatic fixes across many files
|
|
830
|
+
- Iterative test-fix cycles (run tests, analyze, fix, re-run until pass)
|
|
831
|
+
|
|
832
|
+
Do NOT use for (handle these yourself):
|
|
833
|
+
- Tasks completable in fewer than 10 tool calls
|
|
834
|
+
- Creating presentations, documents, spreadsheets
|
|
835
|
+
- Web research or information gathering
|
|
836
|
+
- Simple code review, documentation, or analysis
|
|
837
|
+
- Git operations or simple file edits
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
task: Detailed description of the task for the sub-agent to accomplish
|
|
841
|
+
description: Why you are delegating this task to a sub-agent
|
|
842
|
+
model: Claude model alias or id. Aliases: 'sonnet' (default, fast), 'opus' (powerful,
|
|
843
|
+
slower), 'haiku' (cheapest). Pass an empty string to use the per-CLI default
|
|
844
|
+
(claude -> sonnet). Operator override: CLAUDE_SUB_AGENT_DEFAULT_MODEL env.
|
|
845
|
+
max_turns: Maximum number of agentic turns (default from env, typically 25)
|
|
846
|
+
working_directory: Working directory for the agent (default: /home/assistant)
|
|
847
|
+
resume_session_id: Session ID to resume a previous sub-agent session (from previous result)
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
Sub-agent's response with task results, cost, turn count, and session_id for resume
|
|
851
|
+
|
|
852
|
+
Tip: Run `list-subagent-models` (or `bash /mnt/skills/public/sub-agent/scripts/list_subagent_models.sh`)
|
|
853
|
+
to discover valid model ids for the active SUBAGENT_CLI.
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
_SUBAGENT_DOC_OPENCODE = """Spawn a sub-agent (opencode) to perform a focused subtask.
|
|
857
|
+
|
|
858
|
+
COSTLY: Spawns a separate opencode CLI session with its own API budget.
|
|
859
|
+
Use ONLY as a last resort for complex CODE tasks requiring 10+ iterative tool calls.
|
|
860
|
+
|
|
861
|
+
Justified uses:
|
|
862
|
+
- Multi-file refactoring (5+ files) with test verification loops
|
|
863
|
+
- Complex code review with automatic fixes across many files
|
|
864
|
+
- Iterative test-fix cycles (run tests, analyze, fix, re-run until pass)
|
|
865
|
+
|
|
866
|
+
Do NOT use for (handle these yourself):
|
|
867
|
+
- Tasks completable in fewer than 10 tool calls
|
|
868
|
+
- Creating presentations, documents, spreadsheets
|
|
869
|
+
- Web research or information gathering
|
|
870
|
+
- Simple code review, documentation, or analysis
|
|
871
|
+
- Git operations or simple file edits
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
task: Detailed description of the task for the sub-agent to accomplish
|
|
875
|
+
description: Why you are delegating this task to a sub-agent
|
|
876
|
+
model: opencode model id in `provider/model` form (e.g. `anthropic/claude-sonnet-4-6`,
|
|
877
|
+
`openrouter/qwen/qwen-3-coder`). Pass an empty string to use the per-CLI default
|
|
878
|
+
(controlled by OPENCODE_SUB_AGENT_DEFAULT_MODEL env). Operators can extend the
|
|
879
|
+
alias vocabulary via the OPENCODE_MODEL_ALIASES env var (JSON object string).
|
|
880
|
+
max_turns: Maximum number of agentic turns (default from env, typically 25)
|
|
881
|
+
working_directory: Working directory for the agent (default: /home/assistant)
|
|
882
|
+
resume_session_id: Session ID to resume a previous sub-agent session (from previous result)
|
|
883
|
+
|
|
884
|
+
Returns:
|
|
885
|
+
Sub-agent's response with task results, cost, turn count, and session_id for resume
|
|
886
|
+
|
|
887
|
+
Required: Run `list-subagent-models` (or `bash /mnt/skills/public/sub-agent/scripts/list_subagent_models.sh`)
|
|
888
|
+
to discover valid model ids for opencode before calling this tool. Pass concrete ids; do not use Claude aliases.
|
|
889
|
+
"""
|
|
890
|
+
|
|
891
|
+
_SUBAGENT_DOC_CODEX = """Spawn a sub-agent (codex) to perform a focused subtask.
|
|
892
|
+
|
|
893
|
+
COSTLY: Spawns a separate codex CLI session with its own API budget.
|
|
894
|
+
Use ONLY as a last resort for complex CODE tasks requiring 10+ iterative tool calls.
|
|
895
|
+
|
|
896
|
+
Justified uses:
|
|
897
|
+
- Multi-file refactoring (5+ files) with test verification loops
|
|
898
|
+
- Complex code review with automatic fixes across many files
|
|
899
|
+
- Iterative test-fix cycles (run tests, analyze, fix, re-run until pass)
|
|
900
|
+
|
|
901
|
+
Do NOT use for (handle these yourself):
|
|
902
|
+
- Tasks completable in fewer than 10 tool calls
|
|
903
|
+
- Creating presentations, documents, spreadsheets
|
|
904
|
+
- Web research or information gathering
|
|
905
|
+
- Simple code review, documentation, or analysis
|
|
906
|
+
- Git operations or simple file edits
|
|
907
|
+
|
|
908
|
+
Args:
|
|
909
|
+
task: Detailed description of the task for the sub-agent to accomplish
|
|
910
|
+
description: Why you are delegating this task to a sub-agent
|
|
911
|
+
model: codex model id (fully qualified — codex requires concrete ids, no aliases). Pass an
|
|
912
|
+
empty string to use the per-CLI default (controlled by CODEX_SUB_AGENT_DEFAULT_MODEL
|
|
913
|
+
env). If neither a caller model nor the env var is set, the tool returns an error
|
|
914
|
+
pointing to list-subagent-models for discovery.
|
|
915
|
+
max_turns: Maximum number of agentic turns (default from env, typically 25)
|
|
916
|
+
working_directory: Working directory for the agent (default: /home/assistant)
|
|
917
|
+
resume_session_id: Session ID to resume a previous sub-agent session (from previous result)
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
Sub-agent's response with task results, cost, turn count, and session_id for resume
|
|
921
|
+
|
|
922
|
+
Required: Run `list-subagent-models` (or `bash /mnt/skills/public/sub-agent/scripts/list_subagent_models.sh`)
|
|
923
|
+
to discover valid model ids for codex before calling this tool.
|
|
924
|
+
"""
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _subagent_docstring_for_cli(cli: str) -> str:
|
|
928
|
+
"""Return the FastMCP Tool.description for sub_agent given the active CLI.
|
|
929
|
+
|
|
930
|
+
Falls back to the claude variant for unknown CLI values (mirrors resolve_cli's default).
|
|
931
|
+
"""
|
|
932
|
+
if cli == "opencode":
|
|
933
|
+
return _SUBAGENT_DOC_OPENCODE
|
|
934
|
+
if cli == "codex":
|
|
935
|
+
return _SUBAGENT_DOC_CODEX
|
|
936
|
+
return _SUBAGENT_DOC_CLAUDE
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
async def sub_agent(
|
|
940
|
+
task: str,
|
|
941
|
+
description: str,
|
|
942
|
+
ctx: Context,
|
|
943
|
+
model: str = "",
|
|
944
|
+
max_turns: int = 0,
|
|
945
|
+
working_directory: str = "/home/assistant",
|
|
946
|
+
resume_session_id: str = ""
|
|
947
|
+
) -> str:
|
|
948
|
+
chat_id, error = _validate_chat_id()
|
|
949
|
+
if error:
|
|
950
|
+
return error
|
|
951
|
+
|
|
952
|
+
user_email = current_user_email.get()
|
|
953
|
+
|
|
954
|
+
if max_turns <= 0:
|
|
955
|
+
max_turns = SUB_AGENT_MAX_TURNS
|
|
956
|
+
# Resolve the active CLI runtime — single source of truth.
|
|
957
|
+
cli = resolve_cli()
|
|
958
|
+
|
|
959
|
+
try:
|
|
960
|
+
# Resolve default model inside try so ValueError surfaces as "Sub-agent error: ..."
|
|
961
|
+
# Preserve the resolver's display_name (e.g. "sonnet") so the completion
|
|
962
|
+
# banner shows the friendly alias instead of the full id (e.g.
|
|
963
|
+
# "claude-sonnet-4-6"). dispatch() re-resolves and would only return the
|
|
964
|
+
# id-as-display once the alias has been expanded, losing the friendly name.
|
|
965
|
+
default_display_name = None
|
|
966
|
+
if not model:
|
|
967
|
+
model, default_display_name = resolve_subagent_model("", cli)
|
|
968
|
+
await _ensure_gitlab_token()
|
|
969
|
+
container = await asyncio.to_thread(_get_or_create_container, chat_id)
|
|
970
|
+
|
|
971
|
+
# Write ~/.mcp.json if MCP server names provided via header.
|
|
972
|
+
mcp_servers_str = current_mcp_servers.get()
|
|
973
|
+
if mcp_servers_str:
|
|
974
|
+
mcp_cfg = build_mcp_config(
|
|
975
|
+
mcp_servers_str,
|
|
976
|
+
current_anthropic_base_url.get(),
|
|
977
|
+
user_email or "",
|
|
978
|
+
)
|
|
979
|
+
if mcp_cfg:
|
|
980
|
+
write_cmd = build_mcp_config_write_script(mcp_cfg)
|
|
981
|
+
await asyncio.to_thread(_execute_bash, container, write_cmd, 15)
|
|
982
|
+
|
|
983
|
+
# Build the sub-agent system prompt with dynamic skills.
|
|
984
|
+
file_base_url = f"{PUBLIC_BASE_URL}/files/{chat_id}"
|
|
985
|
+
plan_file = "/home/assistant/task_plan.md"
|
|
986
|
+
skills = (
|
|
987
|
+
skill_manager.get_user_skills_sync(user_email)
|
|
988
|
+
if user_email
|
|
989
|
+
else skill_manager.get_user_skills_sync(None)
|
|
990
|
+
)
|
|
991
|
+
skills_text = skill_manager.build_sub_agent_skills_text(skills)
|
|
992
|
+
|
|
993
|
+
# ANTHROPIC_CUSTOM_HEADERS for LiteLLM user tagging (claude path only;
|
|
994
|
+
# codex/opencode ignore it harmlessly because they don't read it).
|
|
995
|
+
headers_env = ""
|
|
996
|
+
if user_email:
|
|
997
|
+
headers_env = (
|
|
998
|
+
f"ANTHROPIC_CUSTOM_HEADERS="
|
|
999
|
+
f"{shlex.quote(f'x-openwebui-user-email: {user_email}')} "
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
if resume_session_id and cli == Cli.CLAUDE:
|
|
1003
|
+
# Resume path: short prompt, system_prompt empty (claude --resume
|
|
1004
|
+
# restores it from the session).
|
|
1005
|
+
#
|
|
1006
|
+
# Codex/opencode adapters do NOT support session resume — they
|
|
1007
|
+
# emit a stderr warning and start fresh. If we collapsed the
|
|
1008
|
+
# prompt + cleared system_prompt for them too, the "fresh" run
|
|
1009
|
+
# would launch with the generic "Continue working..." stub and
|
|
1010
|
+
# no system context, silently losing the user's task. So gate
|
|
1011
|
+
# the rewrite on claude — codex/opencode keep the original
|
|
1012
|
+
# task + system_prompt and start a fresh, fully-briefed run.
|
|
1013
|
+
task_for_dispatch = (
|
|
1014
|
+
f"Continue working on the task. If needed, re-read "
|
|
1015
|
+
f"{plan_file} for full context."
|
|
1016
|
+
)
|
|
1017
|
+
system_prompt = ""
|
|
1018
|
+
else:
|
|
1019
|
+
# New session: write the task plan file (survives context compaction)
|
|
1020
|
+
# and build the full system prompt with skills.
|
|
1021
|
+
#
|
|
1022
|
+
# SECURITY: encode the task body via base64 + pipe through `base64 -d`
|
|
1023
|
+
# rather than embedding it directly into a shell heredoc. The previous
|
|
1024
|
+
# `cat > {plan_file} << 'TASK_PLAN_EOF'\n{task}\nTASK_PLAN_EOF` pattern
|
|
1025
|
+
# would close early if the task body contained a line equal to the
|
|
1026
|
+
# sentinel `TASK_PLAN_EOF`, causing the remainder to execute as shell
|
|
1027
|
+
# (and corrupting the saved plan even without malice). Base64 is
|
|
1028
|
+
# immune to all shell metacharacters in the payload.
|
|
1029
|
+
# Per CodeRabbit PR#75 review.
|
|
1030
|
+
import base64
|
|
1031
|
+
encoded_task = base64.b64encode(task.encode("utf-8")).decode("ascii")
|
|
1032
|
+
write_plan_cmd = (
|
|
1033
|
+
f"echo {shlex.quote(encoded_task)} | base64 -d > {shlex.quote(plan_file)}"
|
|
1034
|
+
)
|
|
1035
|
+
await asyncio.to_thread(_execute_bash, container, write_plan_cmd, 30)
|
|
1036
|
+
|
|
1037
|
+
system_prompt = f"""<critical_instruction>
|
|
1038
|
+
Your task plan is saved at {plan_file}
|
|
1039
|
+
|
|
1040
|
+
BEFORE ANY ACTION:
|
|
1041
|
+
1. Read {plan_file} to understand your full task
|
|
1042
|
+
2. If context becomes compacted, re-read {plan_file} - it is your source of truth
|
|
1043
|
+
3. The plan file contains all details you need
|
|
1044
|
+
|
|
1045
|
+
Never forget: {plan_file} has your complete instructions.
|
|
1046
|
+
</critical_instruction>
|
|
1047
|
+
|
|
1048
|
+
<environment>
|
|
1049
|
+
You are working in a Linux container (Ubuntu 24) as an autonomous sub-agent.
|
|
1050
|
+
FILE LOCATIONS:
|
|
1051
|
+
- User uploads: /mnt/user-data/uploads (read-only)
|
|
1052
|
+
- Workspace: /home/assistant
|
|
1053
|
+
- Outputs: /mnt/user-data/outputs (URL: {file_base_url}/)
|
|
1054
|
+
</environment>
|
|
1055
|
+
|
|
1056
|
+
<available_skills>
|
|
1057
|
+
IMPORTANT: Read the relevant SKILL.md BEFORE starting any task!
|
|
1058
|
+
|
|
1059
|
+
{skills_text}
|
|
1060
|
+
|
|
1061
|
+
Use `cat <skill-location>` to read skill instructions.
|
|
1062
|
+
</available_skills>"""
|
|
1063
|
+
task_for_dispatch = f"Read and execute your task plan from {plan_file}"
|
|
1064
|
+
|
|
1065
|
+
# Marker file for JSONL session-id discovery — claude-only feature
|
|
1066
|
+
# (codex --ephemeral and opencode run have no equivalent session
|
|
1067
|
+
# JSONL trail; this is gated below in _stream_session_logs).
|
|
1068
|
+
await asyncio.to_thread(
|
|
1069
|
+
_execute_bash, container,
|
|
1070
|
+
"touch /tmp/.sub_agent_start", 5,
|
|
1071
|
+
)
|
|
1072
|
+
start_time = time.time()
|
|
1073
|
+
|
|
1074
|
+
# Stream session logs via tail -f for real-time progress.
|
|
1075
|
+
# Claude-only — codex/opencode get heartbeat-only progress (Phase 7
|
|
1076
|
+
# cost-guardrail-and-ttyd-UX milestone owns the per-CLI progress UX).
|
|
1077
|
+
async def _stream_session_logs():
|
|
1078
|
+
if cli != Cli.CLAUDE:
|
|
1079
|
+
# Heartbeat-only loop for non-claude CLIs.
|
|
1080
|
+
try:
|
|
1081
|
+
while True:
|
|
1082
|
+
await asyncio.sleep(15)
|
|
1083
|
+
elapsed = int(time.time() - start_time)
|
|
1084
|
+
await send_progress(
|
|
1085
|
+
ctx, elapsed, SUB_AGENT_TIMEOUT,
|
|
1086
|
+
f"Agent running... ({format_elapsed_time(elapsed)})",
|
|
1087
|
+
)
|
|
1088
|
+
except asyncio.CancelledError:
|
|
1089
|
+
return
|
|
1090
|
+
except Exception:
|
|
1091
|
+
pass
|
|
1092
|
+
return
|
|
1093
|
+
|
|
1094
|
+
# Claude path — original JSONL log streaming (lifted verbatim
|
|
1095
|
+
# from v0.9.2.0).
|
|
1096
|
+
try:
|
|
1097
|
+
jsonl_path = None
|
|
1098
|
+
for _ in range(60):
|
|
1099
|
+
await asyncio.sleep(1)
|
|
1100
|
+
find_r = await asyncio.to_thread(
|
|
1101
|
+
_execute_bash, container,
|
|
1102
|
+
"find /home/assistant/.claude/projects/-home-assistant/ "
|
|
1103
|
+
"-name '*.jsonl' -newer /tmp/.sub_agent_start "
|
|
1104
|
+
"2>/dev/null | head -1", 5,
|
|
1105
|
+
)
|
|
1106
|
+
path = (find_r.get("output") or "").strip()
|
|
1107
|
+
if path:
|
|
1108
|
+
jsonl_path = path
|
|
1109
|
+
break
|
|
1110
|
+
|
|
1111
|
+
if not jsonl_path:
|
|
1112
|
+
while True:
|
|
1113
|
+
await asyncio.sleep(15)
|
|
1114
|
+
elapsed = int(time.time() - start_time)
|
|
1115
|
+
await send_progress(
|
|
1116
|
+
ctx, elapsed, SUB_AGENT_TIMEOUT,
|
|
1117
|
+
f"Agent running... ({format_elapsed_time(elapsed)})",
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
import threading
|
|
1121
|
+
q = asyncio.Queue()
|
|
1122
|
+
loop = asyncio.get_event_loop()
|
|
1123
|
+
client = get_docker_client()
|
|
1124
|
+
|
|
1125
|
+
def _tail_reader():
|
|
1126
|
+
try:
|
|
1127
|
+
exec_id = client.api.exec_create(
|
|
1128
|
+
container.id, ["tail", "-n", "0", "-f", jsonl_path],
|
|
1129
|
+
stdout=True, stderr=False,
|
|
1130
|
+
)
|
|
1131
|
+
for chunk in client.api.exec_start(exec_id['Id'], stream=True):
|
|
1132
|
+
loop.call_soon_threadsafe(q.put_nowait, chunk)
|
|
1133
|
+
except Exception:
|
|
1134
|
+
pass
|
|
1135
|
+
|
|
1136
|
+
threading.Thread(target=_tail_reader, daemon=True).start()
|
|
1137
|
+
|
|
1138
|
+
buffer = ""
|
|
1139
|
+
while True:
|
|
1140
|
+
try:
|
|
1141
|
+
chunk = await asyncio.wait_for(q.get(), timeout=15)
|
|
1142
|
+
buffer += chunk.decode('utf-8', errors='replace')
|
|
1143
|
+
while '\n' in buffer:
|
|
1144
|
+
line, buffer = buffer.split('\n', 1)
|
|
1145
|
+
action = parse_last_action([line])
|
|
1146
|
+
if action:
|
|
1147
|
+
elapsed = int(time.time() - start_time)
|
|
1148
|
+
msg = f"{action} ({format_elapsed_time(elapsed)})"
|
|
1149
|
+
print(f"[SUB-AGENT-PROGRESS] {msg}")
|
|
1150
|
+
await send_progress(
|
|
1151
|
+
ctx, elapsed, SUB_AGENT_TIMEOUT, msg,
|
|
1152
|
+
)
|
|
1153
|
+
except asyncio.TimeoutError:
|
|
1154
|
+
elapsed = int(time.time() - start_time)
|
|
1155
|
+
await send_progress(
|
|
1156
|
+
ctx, elapsed, SUB_AGENT_TIMEOUT,
|
|
1157
|
+
f"Agent running... ({format_elapsed_time(elapsed)})",
|
|
1158
|
+
)
|
|
1159
|
+
except asyncio.CancelledError:
|
|
1160
|
+
return
|
|
1161
|
+
except Exception:
|
|
1162
|
+
pass
|
|
1163
|
+
|
|
1164
|
+
log_task = asyncio.create_task(_stream_session_logs())
|
|
1165
|
+
try:
|
|
1166
|
+
sub_result, model_id, model_display = await cli_dispatch(
|
|
1167
|
+
container=container,
|
|
1168
|
+
task=task_for_dispatch,
|
|
1169
|
+
system_prompt=system_prompt,
|
|
1170
|
+
model=model,
|
|
1171
|
+
max_turns=max_turns,
|
|
1172
|
+
timeout_s=SUB_AGENT_TIMEOUT,
|
|
1173
|
+
working_directory=working_directory,
|
|
1174
|
+
resume_session_id=resume_session_id,
|
|
1175
|
+
plan_file=plan_file,
|
|
1176
|
+
headers_env=headers_env,
|
|
1177
|
+
)
|
|
1178
|
+
# When caller omitted `model` and we resolved the default above,
|
|
1179
|
+
# restore the friendly display_name (e.g. "sonnet"); dispatch()
|
|
1180
|
+
# would otherwise have surfaced the resolved id ("claude-sonnet-4-6").
|
|
1181
|
+
if default_display_name:
|
|
1182
|
+
model_display = default_display_name
|
|
1183
|
+
finally:
|
|
1184
|
+
log_task.cancel()
|
|
1185
|
+
try:
|
|
1186
|
+
await log_task
|
|
1187
|
+
except asyncio.CancelledError:
|
|
1188
|
+
pass
|
|
1189
|
+
|
|
1190
|
+
duration = time.time() - start_time
|
|
1191
|
+
|
|
1192
|
+
# _find_session_id: claude-only (reads ~/.claude/projects/*.jsonl).
|
|
1193
|
+
async def _find_session_id() -> str:
|
|
1194
|
+
if cli != Cli.CLAUDE:
|
|
1195
|
+
return ""
|
|
1196
|
+
try:
|
|
1197
|
+
find_r = await asyncio.to_thread(
|
|
1198
|
+
_execute_bash, container,
|
|
1199
|
+
"find /home/assistant/.claude/projects/-home-assistant/ "
|
|
1200
|
+
"-name '*.jsonl' -newer /tmp/.sub_agent_start "
|
|
1201
|
+
"2>/dev/null | head -1", 5,
|
|
1202
|
+
)
|
|
1203
|
+
jsonl_path = (find_r.get("output") or "").strip()
|
|
1204
|
+
if jsonl_path:
|
|
1205
|
+
import re
|
|
1206
|
+
m = re.search(r'([0-9a-f-]{36})\.jsonl$', jsonl_path)
|
|
1207
|
+
if m:
|
|
1208
|
+
return m.group(1)
|
|
1209
|
+
except Exception:
|
|
1210
|
+
pass
|
|
1211
|
+
return ""
|
|
1212
|
+
|
|
1213
|
+
# SubAgentResult is the normalised dataclass — render it as the
|
|
1214
|
+
# human-readable string the MCP tool has always returned.
|
|
1215
|
+
# cost_usd is None for codex always, and may be None for opencode —
|
|
1216
|
+
# render "unavailable" rather than "$0.00" (Pitfall 4). Phase 7
|
|
1217
|
+
# cost-guardrail-and-ttyd-UX milestone refines this further.
|
|
1218
|
+
cost_text = (
|
|
1219
|
+
f"${sub_result.cost_usd:.4f}" if sub_result.cost_usd is not None
|
|
1220
|
+
else "unavailable"
|
|
1221
|
+
)
|
|
1222
|
+
turns_text = (
|
|
1223
|
+
f"{sub_result.turns}/{max_turns}" if sub_result.turns is not None
|
|
1224
|
+
else f"?/{max_turns}"
|
|
1225
|
+
)
|
|
1226
|
+
session_id = sub_result.session_id or ""
|
|
1227
|
+
|
|
1228
|
+
# WARNING 1 fix: preserve v0.9.2.0 distinct user-facing messages
|
|
1229
|
+
# for rc=124 (timeout), rc=137 (SIGKILL), rc=143 (SIGTERM), other
|
|
1230
|
+
# non-zero (failed). SubAgentResult.returncode (added in plan
|
|
1231
|
+
# 05-02 Task 0, populated by all three adapters) carries the rc.
|
|
1232
|
+
if sub_result.is_error and not sub_result.text.strip():
|
|
1233
|
+
rc = sub_result.returncode
|
|
1234
|
+
if rc == 124:
|
|
1235
|
+
reason = f"timed out after {SUB_AGENT_TIMEOUT}s"
|
|
1236
|
+
elif rc == 137 or rc == -9:
|
|
1237
|
+
reason = "killed by SIGKILL"
|
|
1238
|
+
elif rc == 143 or rc == -15:
|
|
1239
|
+
reason = "terminated by SIGTERM"
|
|
1240
|
+
elif rc != 0:
|
|
1241
|
+
reason = f"failed with exit code {rc}"
|
|
1242
|
+
else:
|
|
1243
|
+
reason = "crashed before producing results"
|
|
1244
|
+
fallback_session = await _find_session_id()
|
|
1245
|
+
msg = (
|
|
1246
|
+
f"**Sub-Agent Terminated** ({reason})\n"
|
|
1247
|
+
f"**Model:** {model_display} | **Duration:** {duration:.1f}s\n\n"
|
|
1248
|
+
f"Process was killed or crashed before producing results.\n"
|
|
1249
|
+
)
|
|
1250
|
+
if fallback_session:
|
|
1251
|
+
msg += (
|
|
1252
|
+
f"\n**Session ID:** `{fallback_session}` "
|
|
1253
|
+
f"(use resume_session_id to continue)"
|
|
1254
|
+
)
|
|
1255
|
+
return msg
|
|
1256
|
+
|
|
1257
|
+
status = "error" if sub_result.is_error else "success"
|
|
1258
|
+
result_text = (
|
|
1259
|
+
f"**Sub-Agent Completed** ({status})\n"
|
|
1260
|
+
f"**Model:** {model_display} | **Turns:** {turns_text} | "
|
|
1261
|
+
f"**Cost:** {cost_text} | **Duration:** {duration:.1f}s\n\n"
|
|
1262
|
+
f"{sub_result.text}"
|
|
1263
|
+
)
|
|
1264
|
+
if session_id:
|
|
1265
|
+
result_text += (
|
|
1266
|
+
f"\n\n**Session ID:** `{session_id}` "
|
|
1267
|
+
f"(use resume_session_id to continue)"
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
return result_text + _get_default_chat_warning()
|
|
1271
|
+
|
|
1272
|
+
except Exception as e:
|
|
1273
|
+
return f"Sub-agent error: {str(e)}"
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
# Assign the per-CLI docstring BEFORE registering with FastMCP.
|
|
1277
|
+
# FastMCP captures fn.__doc__ at add_tool() time (base.py:66), so this
|
|
1278
|
+
# must come after the function definition and before mcp.add_tool().
|
|
1279
|
+
sub_agent.__doc__ = _subagent_docstring_for_cli(os.getenv("SUBAGENT_CLI", "claude"))
|
|
1280
|
+
mcp.add_tool(sub_agent)
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
# ============================================================================
|
|
1284
|
+
# Helper functions for HTTP header integration
|
|
1285
|
+
# ============================================================================
|
|
1286
|
+
|
|
1287
|
+
def set_context_from_headers(headers: dict):
|
|
1288
|
+
"""Set context variables from HTTP headers.
|
|
1289
|
+
|
|
1290
|
+
Supports both direct headers (x-chat-id) and OpenWebUI headers (x-openwebui-chat-id).
|
|
1291
|
+
Direct headers take priority over OpenWebUI headers.
|
|
1292
|
+
"""
|
|
1293
|
+
# Chat ID (required) - check both formats
|
|
1294
|
+
# Normalize to lowercase: Docker container names are case-sensitive,
|
|
1295
|
+
# and browser URLs may contain uppercase hex in UUIDs
|
|
1296
|
+
if "x-chat-id" in headers:
|
|
1297
|
+
current_chat_id.set(headers["x-chat-id"].lower())
|
|
1298
|
+
elif "x-openwebui-chat-id" in headers:
|
|
1299
|
+
current_chat_id.set(headers["x-openwebui-chat-id"].lower())
|
|
1300
|
+
|
|
1301
|
+
# User email - check both formats
|
|
1302
|
+
if "x-user-email" in headers:
|
|
1303
|
+
current_user_email.set(headers["x-user-email"])
|
|
1304
|
+
elif "x-openwebui-user-email" in headers:
|
|
1305
|
+
current_user_email.set(headers["x-openwebui-user-email"])
|
|
1306
|
+
|
|
1307
|
+
# User name - check both formats (URL-decode: client URL-encodes to handle non-ASCII)
|
|
1308
|
+
if "x-user-name" in headers:
|
|
1309
|
+
current_user_name.set(urllib.parse.unquote(headers["x-user-name"]))
|
|
1310
|
+
elif "x-openwebui-user-name" in headers:
|
|
1311
|
+
current_user_name.set(urllib.parse.unquote(headers["x-openwebui-user-name"]))
|
|
1312
|
+
|
|
1313
|
+
# GitLab token - check both formats
|
|
1314
|
+
if "x-gitlab-token" in headers:
|
|
1315
|
+
current_gitlab_token.set(headers["x-gitlab-token"])
|
|
1316
|
+
elif "x-openwebui-gitlab-token" in headers:
|
|
1317
|
+
current_gitlab_token.set(headers["x-openwebui-gitlab-token"])
|
|
1318
|
+
|
|
1319
|
+
# GitLab host - check both formats
|
|
1320
|
+
if "x-gitlab-host" in headers:
|
|
1321
|
+
current_gitlab_host.set(headers["x-gitlab-host"])
|
|
1322
|
+
elif "x-openwebui-gitlab-host" in headers:
|
|
1323
|
+
current_gitlab_host.set(headers["x-openwebui-gitlab-host"])
|
|
1324
|
+
|
|
1325
|
+
# Anthropic API key - check both formats
|
|
1326
|
+
if "x-anthropic-api-key" in headers:
|
|
1327
|
+
current_anthropic_auth_token.set(headers["x-anthropic-api-key"])
|
|
1328
|
+
elif "x-openwebui-anthropic-api-key" in headers:
|
|
1329
|
+
current_anthropic_auth_token.set(headers["x-openwebui-anthropic-api-key"])
|
|
1330
|
+
|
|
1331
|
+
# Anthropic base URL - check both formats
|
|
1332
|
+
if "x-anthropic-base-url" in headers:
|
|
1333
|
+
current_anthropic_base_url.set(headers["x-anthropic-base-url"])
|
|
1334
|
+
elif "x-openwebui-anthropic-base-url" in headers:
|
|
1335
|
+
current_anthropic_base_url.set(headers["x-openwebui-anthropic-base-url"])
|
|
1336
|
+
|
|
1337
|
+
# MCP Tokens URL - check both formats
|
|
1338
|
+
if "x-mcp-tokens-url" in headers:
|
|
1339
|
+
current_mcp_tokens_url.set(headers["x-mcp-tokens-url"])
|
|
1340
|
+
elif "x-openwebui-mcp-tokens-url" in headers:
|
|
1341
|
+
current_mcp_tokens_url.set(headers["x-openwebui-mcp-tokens-url"])
|
|
1342
|
+
|
|
1343
|
+
# MCP Tokens API key - check both formats
|
|
1344
|
+
if "x-mcp-tokens-api-key" in headers:
|
|
1345
|
+
current_mcp_tokens_api_key.set(headers["x-mcp-tokens-api-key"])
|
|
1346
|
+
elif "x-openwebui-mcp-tokens-api-key" in headers:
|
|
1347
|
+
current_mcp_tokens_api_key.set(headers["x-openwebui-mcp-tokens-api-key"])
|
|
1348
|
+
|
|
1349
|
+
# MCP server names for sub-agent (comma-separated) - check both formats
|
|
1350
|
+
if "x-mcp-servers" in headers:
|
|
1351
|
+
current_mcp_servers.set(headers["x-mcp-servers"])
|
|
1352
|
+
elif "x-openwebui-mcp-servers" in headers:
|
|
1353
|
+
current_mcp_servers.set(headers["x-openwebui-mcp-servers"])
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
class MCPAuthMiddleware:
|
|
1357
|
+
"""ASGI middleware for Bearer token auth on MCP endpoint."""
|
|
1358
|
+
|
|
1359
|
+
def __init__(self, app, api_key: Optional[str] = None):
|
|
1360
|
+
self.app = app
|
|
1361
|
+
self.api_key = api_key
|
|
1362
|
+
|
|
1363
|
+
async def __call__(self, scope, receive, send):
|
|
1364
|
+
if scope["type"] != "http" or not self.api_key:
|
|
1365
|
+
await self.app(scope, receive, send)
|
|
1366
|
+
return
|
|
1367
|
+
|
|
1368
|
+
# Extract Authorization header
|
|
1369
|
+
headers = dict(scope.get("headers", []))
|
|
1370
|
+
auth = headers.get(b"authorization", b"").decode()
|
|
1371
|
+
|
|
1372
|
+
if not auth.startswith("Bearer ") or auth[7:] != self.api_key:
|
|
1373
|
+
# Return 401 Unauthorized
|
|
1374
|
+
response_body = b'{"error": "Unauthorized"}'
|
|
1375
|
+
await send({
|
|
1376
|
+
"type": "http.response.start",
|
|
1377
|
+
"status": 401,
|
|
1378
|
+
"headers": [
|
|
1379
|
+
(b"content-type", b"application/json"),
|
|
1380
|
+
(b"www-authenticate", b"Bearer"),
|
|
1381
|
+
],
|
|
1382
|
+
})
|
|
1383
|
+
await send({
|
|
1384
|
+
"type": "http.response.body",
|
|
1385
|
+
"body": response_body,
|
|
1386
|
+
})
|
|
1387
|
+
return
|
|
1388
|
+
|
|
1389
|
+
await self.app(scope, receive, send)
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
class MCPContextMiddleware:
|
|
1393
|
+
"""ASGI middleware: HTTP headers → ContextVars before MCP handler.
|
|
1394
|
+
|
|
1395
|
+
Also pre-renders the system prompt and stores it in `current_instructions`
|
|
1396
|
+
so the _DynamicInstructionsServer.instructions property can read it
|
|
1397
|
+
synchronously when building InitializeResult (Tier 4).
|
|
1398
|
+
Rendering is cache-backed (60s TTL per (chat_id, user_email)), so real
|
|
1399
|
+
cost on a hot key is a dict lookup.
|
|
1400
|
+
"""
|
|
1401
|
+
|
|
1402
|
+
def __init__(self, app):
|
|
1403
|
+
self.app = app
|
|
1404
|
+
|
|
1405
|
+
async def __call__(self, scope, receive, send):
|
|
1406
|
+
if scope["type"] == "http":
|
|
1407
|
+
headers = {k.decode(): v.decode() for k, v in scope.get("headers", [])}
|
|
1408
|
+
set_context_from_headers(headers)
|
|
1409
|
+
|
|
1410
|
+
# Pre-render system prompt for Tier 4 (dynamic instructions).
|
|
1411
|
+
# Swallow errors — fall back to the static _STATIC_INSTRUCTIONS
|
|
1412
|
+
# string that the _DynamicInstructionsServer getter returns when
|
|
1413
|
+
# current_instructions is None.
|
|
1414
|
+
try:
|
|
1415
|
+
chat_id = current_chat_id.get()
|
|
1416
|
+
user_email = current_user_email.get()
|
|
1417
|
+
rendered = await render_system_prompt(chat_id, user_email)
|
|
1418
|
+
current_instructions.set(rendered)
|
|
1419
|
+
except Exception as e:
|
|
1420
|
+
print(f"[MCP] render_system_prompt warning: {e}")
|
|
1421
|
+
await self.app(scope, receive, send)
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def get_mcp_app(api_key: Optional[str] = None):
|
|
1425
|
+
"""Get the MCP ASGI app with auth and context middleware for mounting."""
|
|
1426
|
+
app = mcp.streamable_http_app()
|
|
1427
|
+
# Wrap with context middleware (inner) then auth (outer)
|
|
1428
|
+
app = MCPContextMiddleware(app)
|
|
1429
|
+
app = MCPAuthMiddleware(app, api_key=api_key)
|
|
1430
|
+
return app
|