From 32b962bb8efa2fcf8678d01534eb66cab46289de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:53:46 +0200 Subject: [PATCH 1/2] Make image builds production-ready --- .env.example | 22 ++--- .gitea/workflows/build-images.yaml | 45 ++++++++++ .gitea/workflows/validate.yaml | 27 ++++++ .github/workflows/build-images.yaml | 35 ++++++-- .github/workflows/validate.yaml | 23 ++++- CHANGELOG.md | 13 ++- README.md | 18 ++-- VERSION | 2 +- docs/credentials-and-brokers.md | 2 +- docs/gitea-packages.md | 11 ++- docs/github-and-gitea.md | 48 ++++++++++ docs/jcb-mcp-roadmap.md | 10 ++- docs/joomla-mcp-study.md | 89 ++++--------------- docs/known-limitations.md | 9 +- docs/mcp-catalog.md | 22 +++-- docs/quick-start.md | 8 +- docs/roadmap.md | 15 ++-- docs/versioning-and-promotion.md | 28 +++--- .../etc/vdm-opencode-platform/authorities.env | 2 +- .../opencode/.config/opencode/opencode.json | 27 ++---- image/provision/common.sh | 27 +++++- image/provision/finalize.sh | 8 +- image/provision/python.sh | 10 +-- image/provision/typescript.sh | 20 ++++- manifest/mcp-catalog.yaml | 37 +++----- manifest/platform.env | 37 ++++---- manifest/sources.lock.yaml | 30 ++++--- manifest/toolchain.env | 29 ++++-- scripts/build-image.sh | 28 +++++- scripts/ci/check-incus-runner.sh | 28 ++++++ scripts/configure-models.sh | 2 + scripts/create-runtime-env.sh | 4 - scripts/import-image-package.sh | 51 +++++++++++ scripts/lib/common.sh | 11 ++- scripts/mcp-toggle.sh | 3 +- scripts/occtl.sh | 2 + scripts/package-image.sh | 47 ++++++++-- scripts/publish-gitea-package.sh | 5 +- scripts/verify-image.sh | 40 ++++++++- tests/browser-smoke.sh | 6 +- tests/fixtures/incus | 31 +++++++ tests/package-roundtrip.sh | 30 +++++++ tests/validate-repository.sh | 43 +++++++-- 43 files changed, 713 insertions(+), 272 deletions(-) create mode 100644 .gitea/workflows/build-images.yaml create mode 100644 .gitea/workflows/validate.yaml create mode 100644 docs/github-and-gitea.md create mode 100755 scripts/ci/check-incus-runner.sh create mode 100755 scripts/import-image-package.sh create mode 100755 tests/fixtures/incus create mode 100755 tests/package-roundtrip.sh diff --git a/.env.example b/.env.example index 5c68511..237f47c 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,13 @@ -# Public, non-secret authority defaults. -VDM_GITEA_BASE_URL=https://git.vdm.dev -VDM_NEXTCLOUD_BASE_URL=https://nextcloud.example.invalid -VDM_GITHUB_BASE_URL=https://github.com +# Host-side, non-secret overrides read by scripts/lib/common.sh. +GITEA_BASE_URL=https://git.vdm.dev +NEXTCLOUD_BASE_URL= +GITHUB_BASE_URL=https://github.com -# Remote MCP endpoints. These should normally point to the trusted MCP gateway, -# not directly to credential-bearing services. -GIT_MCP_URL= -GITHUB_MCP_URL= -GITEA_MCP_URL= -NEXTCLOUD_MCP_URL= -JOOMLA_MCP_URL= -JCB_MCP_URL= -STT_MCP_URL= +# Leave empty to use the Incus default storage pool. +INCUS_STORAGE_POOL= + +# Build-runner safety floor. The full image profile reserves 200 GiB. +MIN_BUILD_FREE_GIB=220 # LLM gateway and local OpenAI-compatible inference endpoint. VDM_LLM_GATEWAY_URL= diff --git a/.gitea/workflows/build-images.yaml b/.gitea/workflows/build-images.yaml new file mode 100644 index 0000000..1820629 --- /dev/null +++ b/.gitea/workflows/build-images.yaml @@ -0,0 +1,45 @@ +name: Build Incus images + +on: + workflow_dispatch: + push: + tags: + - "v*" + +concurrency: + group: incus-images-${{ gitea.ref }} + cancel-in-progress: false + +jobs: + build: + runs-on: incus + timeout-minutes: 240 + strategy: + fail-fast: false + max-parallel: 1 + matrix: + variant: [base, php, python, cpp, typescript, full] + steps: + - name: Check out repository + uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Validate repository + run: ./tests/validate-repository.sh + + - name: Verify Incus build runner + run: ./scripts/ci/check-incus-runner.sh + + - name: Build image + run: ./scripts/build-image.sh "${{ matrix.variant }}" + + - name: Package image + run: ./scripts/package-image.sh "${{ matrix.variant }}" + + - name: Publish Gitea Generic Package + if: ${{ startsWith(gitea.ref, 'refs/tags/v') }} + env: + GITEA_BASE_URL: ${{ vars.GITEA_BASE_URL }} + GITEA_PACKAGE_OWNER: ${{ vars.GITEA_PACKAGE_OWNER }} + GITEA_PACKAGE_USER: ${{ vars.GITEA_PACKAGE_USER }} + GITEA_PACKAGE_TOKEN: ${{ secrets.GITEA_PACKAGE_TOKEN }} + run: ./scripts/publish-gitea-package.sh "${{ matrix.variant }}" diff --git a/.gitea/workflows/validate.yaml b/.gitea/workflows/validate.yaml new file mode 100644 index 0000000..03d5581 --- /dev/null +++ b/.gitea/workflows/validate.yaml @@ -0,0 +1,27 @@ +name: Validate platform + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out repository + uses: https://github.com/actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Install validation dependencies + run: | + apt-get update + apt-get install -y jq shellcheck yamllint + + - name: Validate repository + run: ./tests/validate-repository.sh + + - name: Test image package round trip + run: ./tests/package-roundtrip.sh diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml index 30a4f32..96b1479 100644 --- a/.github/workflows/build-images.yaml +++ b/.github/workflows/build-images.yaml @@ -6,26 +6,51 @@ on: tags: - "v*" +permissions: + contents: read + +concurrency: + group: incus-images-${{ github.ref }} + cancel-in-progress: false + jobs: build: runs-on: [self-hosted, linux, incus] + timeout-minutes: 240 strategy: fail-fast: false + max-parallel: 1 matrix: variant: [base, php, python, cpp, typescript, full] steps: - - uses: actions/checkout@v4 - - name: Validate + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Validate repository run: ./tests/validate-repository.sh - - name: Apply Incus platform - run: ./scripts/apply-incus.sh + + - name: Verify Incus build runner + run: ./scripts/ci/check-incus-runner.sh + - name: Build image run: ./scripts/build-image.sh "${{ matrix.variant }}" + - name: Package image run: ./scripts/package-image.sh "${{ matrix.variant }}" + + - name: Upload downloadable image package + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: vdm-opencode-${{ matrix.variant }}-${{ github.ref_name }} + path: build/packages/*/${{ matrix.variant }}/ + if-no-files-found: error + compression-level: 0 + retention-days: 30 + - name: Publish Gitea Generic Package - if: startsWith(gitea.ref, 'refs/tags/v') + if: ${{ startsWith(github.ref, 'refs/tags/v') && vars.GITEA_PUBLISH_ENABLED == 'true' }} env: + GITEA_BASE_URL: ${{ vars.GITEA_BASE_URL }} GITEA_PACKAGE_OWNER: ${{ vars.GITEA_PACKAGE_OWNER }} GITEA_PACKAGE_USER: ${{ vars.GITEA_PACKAGE_USER }} GITEA_PACKAGE_TOKEN: ${{ secrets.GITEA_PACKAGE_TOKEN }} diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 4d7157d..5c12f25 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -2,16 +2,33 @@ name: Validate platform on: push: + branches: [master] pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: validate-${{ github.ref }} + cancel-in-progress: true jobs: validate: - runs-on: [self-hosted, linux] + runs-on: ubuntu-24.04 + timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install validation dependencies run: | sudo apt-get update sudo apt-get install -y jq shellcheck yamllint - - name: Validate + + - name: Validate repository run: ./tests/validate-repository.sh + + - name: Test image package round trip + run: ./tests/package-roundtrip.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 491b5fe..f2cf63d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.2.0 - 2026-07-22 + +- Split GitHub and Gitea workflows so each uses its own expression context and runner model. +- Moved repository validation to a GitHub-hosted runner and added Incus build-runner preflight checks. +- Added downloadable GitHub workflow artifacts while preserving Gitea Generic Package publication. +- Pinned OpenCode, Git MCP, Playwright, TypeScript, Ruff, mypy and GitHub Action versions. +- Preinstalled Git MCP instead of downloading it dynamically at runtime. +- Added checksummed package manifests and a safe Incus package import helper. +- Removed all third-party Joomla MCP references and all premature Joomla/JCB runtime entries. +- Reserved only the VDM-owned Joomla MCP repository as a deferred integration. + ## 0.1.0 - 2026-07-17 - Initial company platform definition. @@ -7,5 +18,5 @@ - Added repeatable image builders and Gitea package publishing. - Added common OpenCode agent framework. - Added Git and Playwright MCP integration. -- Added gated GitHub, Gitea, Nextcloud, Joomla, JCB and speech MCP definitions. +- Added gated GitHub, Gitea, Nextcloud and speech MCP definitions. - Added broker scaffold, runtime-only credential flow, browser QA and voice transcription. diff --git a/README.md b/README.md index 0bafe6a..09937e7 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ The repository is the authority. Incus images and Gitea packages are generated a - OpenCode agents for orchestration, architecture, implementation, testing, review, security, browser QA, documentation, PHP/Joomla, Python, C/C++, and TypeScript. - Local Git MCP and Playwright browser MCP integration. -- Disabled-by-default remote MCP definitions for GitHub, Gitea, Nextcloud, Joomla, JCB and speech-to-text. +- Disabled-by-default remote MCP definitions for GitHub, Gitea, Nextcloud and speech-to-text. - Runtime-only credential handling under the guest's `/run` tmpfs. - A reference external broker stack for OpenBao, an LLM gateway and a TLS reverse proxy. -- Gitea Actions workflows for validation and image builds. -- Gitea Generic Package publishing scripts. +- Provider-correct GitHub Actions and Gitea Actions workflows. +- Downloadable GitHub workflow artifacts and durable Gitea Generic Package publication. - Image sanitisation and secret-scanning tests. - Host-side voice recording and transcription through any OpenAI-compatible transcription endpoint. @@ -38,12 +38,13 @@ Every image inherits the same security policy and agent framework. ```bash cp .env.example .env -# Edit only public authority URLs here. Do not add secrets. +# Edit only host settings here. Do not add secrets. ./tests/validate-repository.sh ./scripts/bootstrap-host.sh ./scripts/apply-incus.sh ./scripts/build-image.sh php +./scripts/package-image.sh php ./scripts/launch-vm.sh php opencode-llewellyn ./scripts/start-session.sh opencode-llewellyn ``` @@ -67,12 +68,12 @@ For production, point the VM at a trusted external LLM/MCP gateway and issue sho - The reference broker deployment is a scaffold, not a substitute for a security review. - Nextcloud MCP is community software and remains disabled until your team pins and audits a chosen implementation. -- Joomla MCP has strong candidates but remains disabled until the study in `docs/joomla-mcp-study.md` is completed. -- JCB MCP is intentionally a placeholder until the internal server reaches an approved release. +- Joomla MCP is not installed. The only future integration target is + `vast-development-method/joomla-mcp`, after its first reviewed release. +- JCB MCP is not installed and will be added only after the internal repository and first reviewed release exist. - Incus ACLs cannot safely express every hostname-based egress rule. Enforce strict outbound access at a proxy or firewall that supports DNS-aware policy. -- Builds currently support a stable-channel bootstrap with optional expected-version checks. Before production, - populate all version and checksum locks in `manifest/toolchain.env`. +- Incus packages are architecture-specific and must be produced by a trusted hardware-virtualisation runner. ## Documentation @@ -88,4 +89,5 @@ Start with: - `docs/credentials-and-brokers.md` - `docs/scaling-and-operations.md` - `docs/gitea-packages.md` +- `docs/github-and-gitea.md` - `docs/backup-and-migration.md` diff --git a/VERSION b/VERSION index 6e8bf73..0ea3a94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/docs/credentials-and-brokers.md b/docs/credentials-and-brokers.md index 20b7d5a..9897e24 100644 --- a/docs/credentials-and-brokers.md +++ b/docs/credentials-and-brokers.md @@ -20,7 +20,7 @@ A trusted broker host should retain long-lived upstream credentials and expose o - LLM gateway virtual keys with model, rate and spend limits; - GitHub/Gitea MCP sessions limited to selected repositories and operations; - Nextcloud MCP sessions limited to selected users, apps and tagged folders; -- Joomla/JCB MCP sessions defaulting to read-only or non-destructive toolsets. +- future VDM-owned Joomla/JCB integrations, added only after their release gates pass. The VM must never receive a Vaultwarden or OpenBao identity capable of reading all upstream secrets. diff --git a/docs/gitea-packages.md b/docs/gitea-packages.md index de22a8c..7b81e81 100644 --- a/docs/gitea-packages.md +++ b/docs/gitea-packages.md @@ -20,12 +20,19 @@ export GITEA_PACKAGE_TOKEN='runtime-only-token' The publisher token needs package write permission only. Colleagues should receive read-only package access. +Tagged GitHub builds can publish the same package set to Gitea when repository variable +`GITEA_PUBLISH_ENABLED=true`, variable `GITEA_BASE_URL`, owner/user variables and secret +`GITEA_PACKAGE_TOKEN` are configured. Gitea tag builds publish directly through `.gitea/workflows/build-images.yaml`. + ## Import -Download every file in the package version, verify `SHA256SUMS`, then: +Download every file in one package version, then use the checked import helper: ```bash -incus image import ./vdm-opencode-php-0.1.0* --alias vdm-opencode-php/0.1.0 +./scripts/import-image-package.sh ./downloaded-package ``` +The helper verifies every checksum and the manifest before importing the Incus payload. It refuses to overwrite an +existing image alias. + Do not commit exported VM files into Git history. diff --git a/docs/github-and-gitea.md b/docs/github-and-gitea.md new file mode 100644 index 0000000..c460b01 --- /dev/null +++ b/docs/github-and-gitea.md @@ -0,0 +1,48 @@ +# GitHub and Gitea operating model + +## Current production shape + +GitHub can host the source, validate every push and retain downloadable workflow artifacts. Incus image creation +still requires a trusted self-hosted Ubuntu machine with hardware virtualisation, Incus and sufficient storage. +Gitea remains fully supported for source hosting, Actions, its first-party MCP and durable Generic Packages. + +Do not make GitHub and Gitea independently writable authorities for the same branches. Choose one source of truth +and mirror in one direction. + +## Recommended migration path back to Gitea + +1. Keep GitHub as the temporary writable authority while the first `0.2.x` images are proven. +2. Create a private pull mirror in Gitea from + `https://github.com/vast-development-method/opencode-platform` using a read-only fine-grained GitHub token. +3. Enable repository Actions in Gitea. +4. Install Gitea `act_runner` on the dedicated Incus build host, not on the Gitea application server. +5. Register the image runner with the label `incus:host`; the workflow deliberately targets `runs-on: incus`. +6. Configure Gitea Actions variables `GITEA_BASE_URL`, `GITEA_PACKAGE_OWNER`, `GITEA_PACKAGE_USER` and secret + `GITEA_PACKAGE_TOKEN`. +7. Run validation, then manually run the image workflow and import one downloaded package on a clean Incus host. +8. Freeze GitHub writes, force one final mirror sync, convert the Gitea mirror to a regular repository and make + Gitea the writable authority. +9. Configure Gitea to push-mirror the now-authoritative repository to GitHub if GitHub visibility and Actions + validation should remain available. + +## GitHub setup + +The validation workflow needs no repository secrets. For image builds, add one self-hosted runner on the Incus +builder and assign the custom label `incus`. The runner must expose `/dev/kvm`, use an Incus storage pool and have at +least the configured `MIN_BUILD_FREE_GIB` available. Keep the GitHub runner at version `2.327.1` or later because +the pinned current actions use the Node.js 24 action runtime. + +For optional Gitea publication from a GitHub tag build, configure: + +- variable `GITEA_PUBLISH_ENABLED=true`; +- variable `GITEA_BASE_URL=https://git.vdm.dev`; +- variables `GITEA_PACKAGE_OWNER` and `GITEA_PACKAGE_USER`; +- secret `GITEA_PACKAGE_TOKEN` with package-write scope only. + +## Distribution choice + +- GitHub workflow artifacts are convenient build downloads and expire according to repository retention policy. +- GitHub release assets are permanent, but each individual file must remain under 2 GiB. +- Gitea Generic Packages are the preferred durable internal image store. +- A native Incus remote is the fastest option for repeated internal launches and can be added after package + promotion is stable. diff --git a/docs/jcb-mcp-roadmap.md b/docs/jcb-mcp-roadmap.md index 0125369..db1458f 100644 --- a/docs/jcb-mcp-roadmap.md +++ b/docs/jcb-mcp-roadmap.md @@ -1,6 +1,7 @@ # JCB MCP roadmap -The JCB MCP remains disabled until the internal project reaches an approved release. +The JCB MCP is not installed or represented by a runtime endpoint. It will be added only after the internal +repository exists and publishes an approved release. ## Required capability groups @@ -25,7 +26,8 @@ The JCB MCP remains disabled until the internal project reaches an approved rele - Every mutation produces an auditable change set. - Per-project and per-component scope. -## Integration contract +## Integration gate -The platform expects a remote endpoint supplied as `JCB_MCP_URL` and a short-lived token supplied as -`JCB_MCP_TOKEN`. The `jcb` MCP entry remains disabled in the universal image until release approval. +The future release must define its transport, endpoint, short-lived authentication, capability catalogue and +permission model. Only then should a pinned source entry, runtime variables and a disabled-by-default OpenCode MCP +entry be introduced in one reviewed change. diff --git a/docs/joomla-mcp-study.md b/docs/joomla-mcp-study.md index 6c179ac..ea84a1f 100644 --- a/docs/joomla-mcp-study.md +++ b/docs/joomla-mcp-study.md @@ -1,79 +1,26 @@ -# Joomla MCP study +# Joomla MCP integration gate -Reviewed: 2026-07-17 +Reviewed: 2026-07-22 -This repository does not automatically install a Joomla MCP until the company selects and audits an implementation. +The platform will use only the VDM-owned Joomla MCP: -## Candidate A: MCP4Joomla +`https://github.com/vast-development-method/joomla-mcp` -Repository: `nikosdion/joomla-mcp-php` +The repository does not yet contain a reviewed release, so Joomla MCP is deliberately absent from the OpenCode +runtime configuration, session environment template and install process. No third-party Joomla MCP is approved as a +fallback. -Strengths: +## Activation gate -- PHP implementation for Joomla 5.2 and later, including Joomla 6. -- Very broad coverage: the current README reports 249 tools across 22 categories. -- Category and exact-tool filters. -- `--non-destructive` read-only mode. -- Extra forbidden-value protection. -- PHAR and source deployment. -- Extension point for custom tools. +Add the integration only when all of the following are true: -Risks and questions: +- The repository exists and publishes a versioned release. +- The release exposes a documented capability and permission model. +- Joomla service users use ordinary ACLs and are not Super Users by default. +- Read-only and destructive tool groups are independently enforceable. +- Authentication, token rotation, audit redaction and rate limits are tested. +- Joomla 6 compatibility and upgrade behaviour are verified. +- The package reference is immutable and recorded in `manifest/sources.lock.yaml`. +- The MCP entry is added to the image only after the release review passes. -- It expects a Joomla API token for a Super User in its basic configuration. -- A very large tool catalogue can overwhelm model context and broadens the permission surface. -- It is a local stdio process holding a powerful token. -- Company policy should require narrowly scoped Joomla users and explicit category selection. - -## Candidate B: Joomla MCP component - -Repository: `OnepointConsultingLtd/joomla-mcp-server` - -Strengths: - -- Joomla 4, 5 and 6 component exposing HTTP JSON-RPC. -- Current README reports 66 tools. -- Bearer authentication, IP allow-listing, CORS controls, rate limiting, caching and health endpoint. -- Read-only mode and disabled-tool list. -- High-risk extension install/uninstall and template editing tools disabled by default. -- Deliberately excludes user management and global configuration. - -Risks and questions: - -- A component installed in the Joomla site increases the site's own attack surface. -- Some operations use database or filesystem APIs when Web Services are insufficient. -- Remote endpoint lifecycle, upgrade and vulnerability response must be owned. -- The internal API token and external bearer token both require rotation procedures. - -## Provisional direction - -Use the component model as the preferred remote architecture for company operation because it keeps site-specific -logic beside Joomla and supports network controls. Use MCP4Joomla as an important reference for broad Joomla API -coverage, filtering and extensibility. - -A VDM-owned Joomla MCP should combine: - -- remote streamable HTTP; -- ordinary Joomla ACL enforcement; -- dedicated service users rather than Super User by default; -- per-tool and per-category policy; -- read-only default; -- dangerous-tool compile-time or administrator gating; -- audit records with parameter redaction; -- pagination and response-size limits; -- idempotency and dry-run support; -- health, version and capability discovery; -- test fixtures for Joomla 4/5/6 as required; -- extension hooks for JCB without coupling core Joomla tools to JCB. - -## Approval checklist - -- Threat model complete. -- Tool inventory reviewed. -- No token returned in logs or errors. -- Read-only role tested. -- Destructive tools require explicit policy. -- Prompt-injection tests complete. -- Rate and response limits tested. -- Release artefacts signed and reproducible. -- Upgrade and rollback documented. +Until then, Joomla work continues through the PHP image, the Joomla CLI/API, ordinary browser testing and Git. diff --git a/docs/known-limitations.md b/docs/known-limitations.md index 6a05140..06df2dd 100644 --- a/docs/known-limitations.md +++ b/docs/known-limitations.md @@ -2,11 +2,12 @@ - The repository has been statically validated, but the image build was not executed in this delivery environment because nested Incus and external package downloads are unavailable here. -- Upstream tool versions still contain development-channel floating references. +- Ubuntu/Node/PHP system packages are resolved from the configured repositories at build time; their exact installed + versions are recorded inside each image manifest but are not yet sourced from a company snapshot mirror. - The Nextcloud candidate has not received a VDM security audit. -- No Joomla MCP candidate is enabled. -- JCB MCP has no approved release. +- The VDM Joomla MCP and JCB MCP are intentionally not installed pending their first approved releases. - The reference broker does not yet implement the company MCP authorization gateway. - Incus ACLs currently provide inbound isolation but not strict DNS-aware outbound filtering. -- Gitea Actions require a self-hosted runner with Incus access and hardware virtualization. +- GitHub image builds require a self-hosted runner labelled `self-hosted`, `linux`, `incus`. Gitea image builds + require a host runner labelled `incus:host`. - OpenCode config compatibility should be revalidated whenever OpenCode is upgraded. diff --git a/docs/mcp-catalog.md b/docs/mcp-catalog.md index 7eac0ba..64acb21 100644 --- a/docs/mcp-catalog.md +++ b/docs/mcp-catalog.md @@ -5,12 +5,13 @@ ### Git Uses `mcp-server-git` from the Model Context Protocol project. It can inspect and modify repositories and is still -described upstream as early development. It is constrained to `/workspace`. +described upstream as early development. Version `2026.7.10` is preinstalled and constrained to `/workspace`. ### Playwright Microsoft's Playwright MCP is installed in PHP, TypeScript and full images. Keep it for exploratory, persistent -browser QA. For large deterministic suites, ordinary Playwright tests or the Playwright CLI are more token-efficient. +browser QA. The package is pinned and runs with an isolated browser profile. For large deterministic suites, +ordinary Playwright tests or the Playwright CLI are more token-efficient. ## Ready but disabled @@ -22,9 +23,10 @@ Limit toolsets; the full server can add substantial context and permissions. ### Gitea Use Gitea's own `https://gitea.com/gitea/gitea-mcp`. The company authority defaults to `https://git.vdm.dev`. -Pin an audited release and expose it through the MCP gateway. +The platform keeps its runtime endpoint and short-lived token support. Expose the audited deployment through the +company MCP gateway. -## Candidates requiring approval +## Candidate requiring approval ### Nextcloud @@ -32,13 +34,17 @@ Pin an audited release and expose it through the MCP gateway. Contacts, Deck, Notes, Tables, Talk and other coverage, supports streamable HTTP and Login Flow v2, and provides tag-based exclusion. It remains community software and needs code, dependency and permission review. -### Joomla +## Deferred VDM integrations -See `docs/joomla-mcp-study.md`. +### Joomla MCP -### JCB +No Joomla MCP is installed or configured in the images. The only approved future source is +`https://github.com/vast-development-method/joomla-mcp`. See `docs/joomla-mcp-study.md`. -See `docs/jcb-mcp-roadmap.md`. +### JCB MCP + +No JCB MCP is installed or configured. Integration starts only after the internal repository and its first reviewed +release are available. See `docs/jcb-mcp-roadmap.md`. ## Context control diff --git a/docs/quick-start.md b/docs/quick-start.md index 06a223a..3ee7aa6 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -3,19 +3,21 @@ ## 1. Clone and validate ```bash -git clone ssh://git@git.vdm.dev/platform/vdm-opencode-platform.git +git clone https://github.com/vast-development-method/opencode-platform.git cd vdm-opencode-platform cp .env.example .env ./tests/validate-repository.sh ``` -Edit `VDM_NEXTCLOUD_BASE_URL` before enabling Nextcloud integration. Do not place secrets in `.env`. +Use `GITEA_BASE_URL` and `INCUS_STORAGE_POOL` in `.env` when the defaults do not match the host. Do not place +secrets in `.env`. ## 2. Prepare Incus ```bash ./scripts/bootstrap-host.sh ./scripts/apply-incus.sh +./scripts/ci/check-incus-runner.sh ``` ## 3. Build one image @@ -62,7 +64,7 @@ The file is under `/run/user/$UID`, which is tmpfs. Obtain only short-lived scop ./scripts/mcp-toggle.sh opencode-llewellyn playwright true ``` -Do not enable Joomla or JCB until their servers are approved. +Joomla and JCB are intentionally absent and cannot be enabled until their VDM-owned releases are approved. ## 7. Start OpenCode diff --git a/docs/roadmap.md b/docs/roadmap.md index cdc3f4e..0e15913 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,12 +1,12 @@ # Roadmap -## Phase 1: Baseline +## Phase 1: Baseline — implemented -- Validate repository on Ubuntu builder. -- Build PHP and TypeScript images. +- Validate the repository on GitHub-hosted and Gitea runners. +- Build all six variants on a trusted Incus runner. - Pilot local Git and Playwright MCP. - Configure ChatGPT Plus, Anthropic API, xAI API and local Llama routes. -- Publish image packages to Gitea. +- Expose short-lived GitHub build artifacts and publish durable image packages to Gitea. ## Phase 2: Broker @@ -16,12 +16,11 @@ - Enforce DNS-aware egress policy. - Add central audit and revocation tests. -## Phase 3: Joomla and JCB +## Phase 3: VDM Joomla and JCB -- Complete Joomla MCP evaluation. -- Define VDM Joomla MCP architecture. +- Release and review `vast-development-method/joomla-mcp`. - Complete JCB MCP protocol and test suite. -- Add read-only pilot integrations. +- Add each integration only after its own activation gate passes. ## Phase 4: Fleet diff --git a/docs/versioning-and-promotion.md b/docs/versioning-and-promotion.md index a246342..b90b268 100644 --- a/docs/versioning-and-promotion.md +++ b/docs/versioning-and-promotion.md @@ -3,29 +3,23 @@ The repository version, image alias, Gitea package version and release tag must match. ```text -Repository tag: v0.1.0 -Incus alias: vdm-opencode-php/0.1.0 -Gitea package: vdm-opencode-php/0.1.0 +Repository tag: v0.2.0 +Incus alias: vdm-opencode-php/0.2.0 +Gitea package: vdm-opencode-php/0.2.0 Build manifest: /etc/vdm-opencode-platform/build.json ``` -## Development channel - -Floating upstream references may be used only while the platform is being explored. The resulting image must not -be promoted to company production. - ## Release channel Before a release: -1. Replace `REVIEW_AND_PIN` entries in `manifest/sources.lock.yaml`. -2. Replace `@latest` package references with exact approved versions. -3. Mirror or archive upstream artefacts where licensing permits. -4. Record SHA-256 checksums. -5. Build from a clean runner. -6. Run repository, guest, browser and security tests. -7. Export and checksum the image. -8. Pilot the image. -9. Tag the repository and publish the matching package version. +1. Review and deliberately update `manifest/toolchain.env` and `manifest/sources.lock.yaml`. +2. Confirm that no floating dependency or mutable GitHub Action reference was introduced. +3. Build from a clean, trusted Incus runner. +4. Run repository, guest, browser and security tests. +5. Export the image and verify the generated manifest and `SHA256SUMS`. +6. Pilot the image. +7. Tag the repository with the exact value from `VERSION` prefixed by `v`. +8. Retain the GitHub workflow artifact as a short-lived download and publish the durable package to Gitea. Do not mutate an existing released image alias. Publish a new semantic version. diff --git a/image/files/etc/vdm-opencode-platform/authorities.env b/image/files/etc/vdm-opencode-platform/authorities.env index ef8d2c4..b04315e 100644 --- a/image/files/etc/vdm-opencode-platform/authorities.env +++ b/image/files/etc/vdm-opencode-platform/authorities.env @@ -1,3 +1,3 @@ GITEA_BASE_URL=https://git.vdm.dev -NEXTCLOUD_BASE_URL=https://nextcloud.example.invalid +NEXTCLOUD_BASE_URL= GITHUB_BASE_URL=https://github.com diff --git a/image/files/home/opencode/.config/opencode/opencode.json b/image/files/home/opencode/.config/opencode/opencode.json index 160c6dd..449eeea 100644 --- a/image/files/home/opencode/.config/opencode/opencode.json +++ b/image/files/home/opencode/.config/opencode/opencode.json @@ -3,13 +3,16 @@ "mcp": { "git": { "type": "local", - "command": ["uvx", "mcp-server-git", "--repository", "/workspace"], + "command": ["/home/opencode/.local/bin/mcp-server-git", "--repository", "/workspace"], "enabled": true, "timeout": 15000 }, "playwright": { "type": "local", - "command": ["npx", "-y", "@playwright/mcp@latest", "--headless"], + "command": ["/usr/local/bin/vdm-playwright-mcp", "--headless", "--isolated", "--output-dir", "/workspace/.artifacts/browser"], + "environment": { + "PLAYWRIGHT_BROWSERS_PATH": "/opt/ms-playwright" + }, "enabled": false, "timeout": 30000 }, @@ -43,26 +46,6 @@ "enabled": false, "timeout": 30000 }, - "joomla": { - "type": "remote", - "url": "{env:JOOMLA_MCP_URL}", - "oauth": false, - "headers": { - "Authorization": "Bearer {env:JOOMLA_MCP_TOKEN}" - }, - "enabled": false, - "timeout": 30000 - }, - "jcb": { - "type": "remote", - "url": "{env:JCB_MCP_URL}", - "oauth": false, - "headers": { - "Authorization": "Bearer {env:JCB_MCP_TOKEN}" - }, - "enabled": false, - "timeout": 30000 - }, "speech_to_text": { "type": "remote", "url": "{env:STT_MCP_URL}", diff --git a/image/provision/common.sh b/image/provision/common.sh index 94fbfca..4b2e764 100755 --- a/image/provision/common.sh +++ b/image/provision/common.sh @@ -49,10 +49,33 @@ if ! command -v uv >/dev/null 2>&1; then curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh fi -sudo -u "$AGENT_USER" -H env OPENCODE_INSTALL_DIR="$AGENT_HOME/.local/bin" \ - bash -c 'curl -fsSL "${OPENCODE_INSTALL_URL:-https://opencode.ai/install}" | bash' +npm install --global "${OPENCODE_PACKAGE:?OPENCODE_PACKAGE is required}" +opencode_path="$(command -v opencode)" +ln -sfn "$opencode_path" "$AGENT_HOME/.local/bin/opencode" + +sudo -u "$AGENT_USER" -H env \ + UV_TOOL_BIN_DIR="$AGENT_HOME/.local/bin" \ + UV_TOOL_DIR="$AGENT_HOME/.local/share/uv/tools" \ + uv tool install --force "${GIT_MCP_PACKAGE:?GIT_MCP_PACKAGE is required}" test -x "$AGENT_HOME/.local/bin/opencode" +test -x "$AGENT_HOME/.local/bin/mcp-server-git" + +installed_opencode="$("$AGENT_HOME/.local/bin/opencode" --version | tr -d '[:space:]')" +[ "$installed_opencode" = "$OPENCODE_EXPECTED_VERSION" ] || { + printf 'OpenCode version mismatch: expected %s, got %s\n' \ + "$OPENCODE_EXPECTED_VERSION" "$installed_opencode" >&2 + exit 1 +} + +installed_git_mcp="$(sudo -u "$AGENT_USER" -H \ + "$AGENT_HOME/.local/share/uv/tools/mcp-server-git/bin/python" -c \ + 'import importlib.metadata; print(importlib.metadata.version("mcp-server-git"))')" +[ "$installed_git_mcp" = "$GIT_MCP_EXPECTED_VERSION" ] || { + printf 'Git MCP version mismatch: expected %s, got %s\n' \ + "$GIT_MCP_EXPECTED_VERSION" "$installed_git_mcp" >&2 + exit 1 +} git config --system init.defaultBranch main git config --system fetch.prune true diff --git a/image/provision/finalize.sh b/image/provision/finalize.sh index 79fe6d2..3989794 100755 --- a/image/provision/finalize.sh +++ b/image/provision/finalize.sh @@ -8,13 +8,18 @@ AGENT_HOME="/home/${AGENT_USER}" install -d -m 0755 /etc/vdm-opencode-platform +# shellcheck disable=SC1091 +source /etc/os-release +os_label="${ID:-unknown} ${VERSION_ID:-unknown}" + jq -n \ --arg platform "vdm-opencode-platform" \ --arg version "$PLATFORM_VERSION" \ --arg variant "$VARIANT" \ --arg built_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - --arg os "$(. /etc/os-release && printf '%s %s' "$ID" "$VERSION_ID")" \ + --arg os "$os_label" \ --arg opencode "$(sudo -u "$AGENT_USER" -H "$AGENT_HOME/.local/bin/opencode" --version 2>/dev/null || true)" \ + --arg git_mcp "$(sudo -u "$AGENT_USER" -H "$AGENT_HOME/.local/share/uv/tools/mcp-server-git/bin/python" -c 'import importlib.metadata; print(importlib.metadata.version("mcp-server-git"))' 2>/dev/null || true)" \ --arg node "$(node --version 2>/dev/null || true)" \ --arg php "$(php -r 'echo PHP_VERSION;' 2>/dev/null || true)" \ --arg python "$(python3 --version 2>/dev/null || true)" \ @@ -26,6 +31,7 @@ jq -n \ os: $os, toolchain: { opencode: $opencode, + git_mcp: $git_mcp, node: $node, php: $php, python: $python diff --git a/image/provision/python.sh b/image/provision/python.sh index 4b712d6..800fedb 100755 --- a/image/provision/python.sh +++ b/image/provision/python.sh @@ -7,8 +7,8 @@ apt-get install -y --no-install-recommends \ python3-dev \ python3-pytest -for tool in ruff mypy; do - if ! sudo -u opencode -H uv tool list | grep -q "^${tool} "; then - sudo -u opencode -H uv tool install "$tool" - fi -done +sudo -u opencode -H uv tool install --force "${RUFF_PACKAGE:?RUFF_PACKAGE is required}" +sudo -u opencode -H uv tool install --force "${MYPY_PACKAGE:?MYPY_PACKAGE is required}" + +[ "$(sudo -u opencode -H /home/opencode/.local/bin/ruff --version | awk '{print $2}')" = "$RUFF_EXPECTED_VERSION" ] +[ "$(sudo -u opencode -H /home/opencode/.local/bin/mypy --version | awk '{print $2}')" = "$MYPY_EXPECTED_VERSION" ] diff --git a/image/provision/typescript.sh b/image/provision/typescript.sh index 551053a..2788dc3 100755 --- a/image/provision/typescript.sh +++ b/image/provision/typescript.sh @@ -3,8 +3,24 @@ set -Eeuo pipefail export DEBIAN_FRONTEND=noninteractive corepack enable -npm install --global typescript tsx "${PLAYWRIGHT_MCP_PACKAGE:-@playwright/mcp@latest}" "${PLAYWRIGHT_PACKAGE:-playwright@latest}" -npx playwright install --with-deps chromium +npm install --global \ + "${TYPESCRIPT_PACKAGE:?TYPESCRIPT_PACKAGE is required}" \ + "${TSX_PACKAGE:?TSX_PACKAGE is required}" \ + "${PLAYWRIGHT_MCP_PACKAGE:?PLAYWRIGHT_MCP_PACKAGE is required}" \ + "${PLAYWRIGHT_PACKAGE:?PLAYWRIGHT_PACKAGE is required}" +install -d -m 0755 /opt/ms-playwright +PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright npx playwright install --with-deps chromium + +playwright_mcp_path="$(command -v playwright-mcp)" +ln -sfn "$playwright_mcp_path" /usr/local/bin/vdm-playwright-mcp + +[ "$(tsc --version | awk '{print $2}')" = "$TYPESCRIPT_EXPECTED_VERSION" ] +[ "$(tsx --version | awk 'NR == 1 {print $2}')" = "$TSX_EXPECTED_VERSION" ] +[ "$(playwright --version | awk '{print $2}')" = "$PLAYWRIGHT_EXPECTED_VERSION" ] + +installed_mcp="$(npm list --global --json --depth=0 | jq -r '.dependencies["@playwright/mcp"].version')" +[ "$installed_mcp" = "$PLAYWRIGHT_MCP_EXPECTED_VERSION" ] +test -x /usr/local/bin/vdm-playwright-mcp CONFIG=/home/opencode/.config/opencode/opencode.json tmp="$(mktemp)" diff --git a/manifest/mcp-catalog.yaml b/manifest/mcp-catalog.yaml index bad2549..46dbd4d 100644 --- a/manifest/mcp-catalog.yaml +++ b/manifest/mcp-catalog.yaml @@ -1,5 +1,5 @@ schema: 1 -reviewed_on: 2026-07-17 +reviewed_on: 2026-07-22 servers: git: status: enabled @@ -21,42 +21,33 @@ servers: transport: remote-or-stdio source: https://gitea.com/gitea/gitea-mcp default_authority: https://git.vdm.dev - note: Pin an audited release before enabling in company images. + note: Preserve this integration; expose it through the trusted company MCP gateway with a scoped identity. nextcloud: status: candidate-disabled trust: community transport: remote-preferred source: https://github.com/cbcoutinho/nextcloud-mcp-server - default_authority: https://nextcloud.example.invalid - note: Strong candidate with broad app coverage; audit and pin before adoption. + default_authority: operator-supplied + note: Audit and pin before adoption. playwright: status: enabled-on-web-images trust: first-party transport: stdio source: https://github.com/microsoft/playwright-mcp package: "@playwright/mcp" - note: Prefer Playwright CLI plus skills for high-throughput deterministic test suites; keep MCP for exploratory QA. - joomla_mcp4joomla: - status: candidate-disabled - trust: community - transport: stdio - source: https://github.com/nikosdion/joomla-mcp-php - note: Very broad Joomla API coverage and category/read-only filtering. - joomla_component: - status: candidate-disabled - trust: community - transport: remote - source: https://github.com/OnepointConsultingLtd/joomla-mcp-server - note: Joomla component with HTTP endpoint, rate limiting, allow-listing and deliberately limited high-risk tools. - jcb: - status: internal-unfinished - trust: internal - transport: remote-preferred - source: TODO - note: Keep disabled until protocol, permissions, tests and release process are approved. + note: Prefer deterministic Playwright tests for suites; keep MCP for exploratory QA. speech_to_text: status: optional trust: internal-endpoint transport: command-or-remote source: internal OpenAI-compatible transcription endpoint note: Host microphone capture should remain outside the VM; inject only transcribed text. + +deferred_integrations: + joomla: + status: not-installed + source: https://github.com/vast-development-method/joomla-mcp + activation_gate: Add only after the first VDM-reviewed release is available. + jcb: + status: not-installed + activation_gate: Add only after the VDM repository and first reviewed release are available. diff --git a/manifest/platform.env b/manifest/platform.env index f469f8a..44efae7 100644 --- a/manifest/platform.env +++ b/manifest/platform.env @@ -1,23 +1,24 @@ -PLATFORM_NAME=vdm-opencode-platform -PLATFORM_VERSION=0.1.0 +PLATFORM_NAME="${PLATFORM_NAME:-vdm-opencode-platform}" +PLATFORM_VERSION="${PLATFORM_VERSION:-0.2.0}" -INCUS_PROJECT=vdm-agents -INCUS_BUILD_NETWORK=vdm-buildbr0 -INCUS_RUNTIME_NETWORK=vdm-agentbr0 -INCUS_RUNTIME_ACL=vdm-agent-runtime +INCUS_PROJECT="${INCUS_PROJECT:-vdm-agents}" +INCUS_BUILD_NETWORK="${INCUS_BUILD_NETWORK:-vdm-buildbr0}" +INCUS_RUNTIME_NETWORK="${INCUS_RUNTIME_NETWORK:-vdm-agentbr0}" +INCUS_RUNTIME_ACL="${INCUS_RUNTIME_ACL:-vdm-agent-runtime}" -INCUS_BASE_IMAGE=images:ubuntu/24.04/cloud -INCUS_IMAGE_PREFIX=vdm-opencode -INCUS_STORAGE_POOL= -INCUS_DEFAULT_ROOT_SIZE=80GiB +INCUS_BASE_IMAGE="${INCUS_BASE_IMAGE:-images:ubuntu/24.04/cloud}" +INCUS_IMAGE_PREFIX="${INCUS_IMAGE_PREFIX:-vdm-opencode}" +INCUS_STORAGE_POOL="${INCUS_STORAGE_POOL:-}" +INCUS_DEFAULT_ROOT_SIZE="${INCUS_DEFAULT_ROOT_SIZE:-80GiB}" -AGENT_USER=opencode -AGENT_HOME=/home/opencode -WORKSPACE_ROOT=/workspace +AGENT_USER="${AGENT_USER:-opencode}" +AGENT_HOME="${AGENT_HOME:-/home/opencode}" +WORKSPACE_ROOT="${WORKSPACE_ROOT:-/workspace}" -GITEA_BASE_URL=https://git.vdm.dev -NEXTCLOUD_BASE_URL=https://nextcloud.example.invalid -GITHUB_BASE_URL=https://github.com +GITEA_BASE_URL="${GITEA_BASE_URL:-https://git.vdm.dev}" +NEXTCLOUD_BASE_URL="${NEXTCLOUD_BASE_URL:-}" +GITHUB_BASE_URL="${GITHUB_BASE_URL:-https://github.com}" -DEFAULT_SESSION_TTL=2h -DEFAULT_IMAGE_RETENTION=5 +DEFAULT_SESSION_TTL="${DEFAULT_SESSION_TTL:-2h}" +DEFAULT_IMAGE_RETENTION="${DEFAULT_IMAGE_RETENTION:-5}" +MIN_BUILD_FREE_GIB="${MIN_BUILD_FREE_GIB:-220}" diff --git a/manifest/sources.lock.yaml b/manifest/sources.lock.yaml index 09bf9d8..932bb7c 100644 --- a/manifest/sources.lock.yaml +++ b/manifest/sources.lock.yaml @@ -1,5 +1,5 @@ schema: 1 -generated_on: 2026-07-17 +generated_on: 2026-07-22 policy: require_immutable_refs_for_release: true allow_floating_refs_for_development: true @@ -7,7 +7,8 @@ sources: opencode: repository: https://github.com/anomalyco/opencode inspected_ref: efb6cc2d4bf6332eb156709795d2b3a649198b65 - build_ref: REVIEW_AND_PIN + package: opencode-ai + build_ref: 1.18.4 incus: repository: https://github.com/lxc/incus inspected_ref: 5f70b59f323cb8750e3c1dd16213fedf2679d576 @@ -15,22 +16,27 @@ sources: git_mcp: repository: https://github.com/modelcontextprotocol/servers path: src/git - build_ref: REVIEW_AND_PIN + build_ref: v2026.7.10 github_mcp: repository: https://github.com/github/github-mcp-server - build_ref: REVIEW_AND_PIN + build_ref: REMOTE_NOT_BUNDLED gitea_mcp: repository: https://gitea.com/gitea/gitea-mcp - build_ref: REVIEW_AND_PIN + build_ref: REMOTE_NOT_BUNDLED playwright_mcp: repository: https://github.com/microsoft/playwright-mcp - build_ref: REVIEW_AND_PIN + package: "@playwright/mcp" + build_ref: 0.0.78 nextcloud_mcp: repository: https://github.com/cbcoutinho/nextcloud-mcp-server build_ref: DISABLED_UNTIL_AUDITED - joomla_mcp4joomla: - repository: https://github.com/nikosdion/joomla-mcp-php - build_ref: DISABLED_UNTIL_SELECTED - joomla_component_mcp: - repository: https://github.com/OnepointConsultingLtd/joomla-mcp-server - build_ref: DISABLED_UNTIL_SELECTED +deferred_integrations: + joomla_mcp: + repository: https://github.com/vast-development-method/joomla-mcp + build_ref: DISABLED_UNTIL_FIRST_RELEASE + jcb_mcp: + build_ref: DISABLED_UNTIL_REPOSITORY_AND_RELEASE_EXIST + +github_actions: + checkout: 3d3c42e5aac5ba805825da76410c181273ba90b1 + upload_artifact: bbbca2ddaa5d8feaa63e36b76fdaad77386f024f diff --git a/manifest/toolchain.env b/manifest/toolchain.env index 991b45a..0572019 100644 --- a/manifest/toolchain.env +++ b/manifest/toolchain.env @@ -1,20 +1,31 @@ -# Production builds should replace every floating value with an approved exact version and checksum. -# Empty EXPECTED values mean "record what was installed but do not enforce a version". +# Approved package versions for platform release 0.2.0. +# Update these values deliberately and validate every image before promotion. -OPENCODE_INSTALL_URL=https://opencode.ai/install -OPENCODE_EXPECTED_VERSION= +OPENCODE_PACKAGE=opencode-ai@1.18.4 +OPENCODE_EXPECTED_VERSION=1.18.4 NODE_MAJOR=22 PHP_MAJOR=8.3 PYTHON_MAJOR=3.12 JAVA_MAJOR=21 -GIT_MCP_PACKAGE=mcp-server-git -GIT_MCP_EXPECTED_VERSION= +GIT_MCP_PACKAGE=mcp-server-git==2026.7.10 +GIT_MCP_EXPECTED_VERSION=2026.7.10 -PLAYWRIGHT_MCP_PACKAGE=@playwright/mcp@latest -PLAYWRIGHT_PACKAGE=playwright@latest -PLAYWRIGHT_EXPECTED_VERSION= +PLAYWRIGHT_MCP_PACKAGE=@playwright/mcp@0.0.78 +PLAYWRIGHT_MCP_EXPECTED_VERSION=0.0.78 +PLAYWRIGHT_PACKAGE=playwright@1.61.1 +PLAYWRIGHT_EXPECTED_VERSION=1.61.1 + +TYPESCRIPT_PACKAGE=typescript@7.0.2 +TYPESCRIPT_EXPECTED_VERSION=7.0.2 +TSX_PACKAGE=tsx@4.23.1 +TSX_EXPECTED_VERSION=4.23.1 + +RUFF_PACKAGE=ruff==0.15.22 +RUFF_EXPECTED_VERSION=0.15.22 +MYPY_PACKAGE=mypy==2.3.0 +MYPY_EXPECTED_VERSION=2.3.0 OPENBAO_IMAGE= LITELLM_IMAGE= diff --git a/scripts/build-image.sh b/scripts/build-image.sh index ebfa427..ab20518 100755 --- a/scripts/build-image.sh +++ b/scripts/build-image.sh @@ -33,9 +33,22 @@ tar -C "$ROOT_DIR/image/provision" -cf - . | project_cmd exec "$BUILD_NAME" -- t COMMON_ENV=( "AGENT_USER=$AGENT_USER" "NODE_MAJOR=$NODE_MAJOR" - "OPENCODE_INSTALL_URL=$OPENCODE_INSTALL_URL" + "OPENCODE_PACKAGE=$OPENCODE_PACKAGE" + "OPENCODE_EXPECTED_VERSION=$OPENCODE_EXPECTED_VERSION" + "GIT_MCP_PACKAGE=$GIT_MCP_PACKAGE" + "GIT_MCP_EXPECTED_VERSION=$GIT_MCP_EXPECTED_VERSION" "PLAYWRIGHT_MCP_PACKAGE=$PLAYWRIGHT_MCP_PACKAGE" + "PLAYWRIGHT_MCP_EXPECTED_VERSION=$PLAYWRIGHT_MCP_EXPECTED_VERSION" "PLAYWRIGHT_PACKAGE=$PLAYWRIGHT_PACKAGE" + "PLAYWRIGHT_EXPECTED_VERSION=$PLAYWRIGHT_EXPECTED_VERSION" + "TYPESCRIPT_PACKAGE=$TYPESCRIPT_PACKAGE" + "TYPESCRIPT_EXPECTED_VERSION=$TYPESCRIPT_EXPECTED_VERSION" + "TSX_PACKAGE=$TSX_PACKAGE" + "TSX_EXPECTED_VERSION=$TSX_EXPECTED_VERSION" + "RUFF_PACKAGE=$RUFF_PACKAGE" + "RUFF_EXPECTED_VERSION=$RUFF_EXPECTED_VERSION" + "MYPY_PACKAGE=$MYPY_PACKAGE" + "MYPY_EXPECTED_VERSION=$MYPY_EXPECTED_VERSION" ) exec_with_env() { @@ -48,9 +61,16 @@ exec_with_env() { } exec_with_env bash /opt/vdm-build/common.sh -if [ "$VARIANT" != base ]; then - exec_with_env bash "/opt/vdm-build/${VARIANT}.sh" -fi +case "$VARIANT" in + base) ;; + php) + exec_with_env bash /opt/vdm-build/php.sh + exec_with_env bash /opt/vdm-build/typescript.sh + ;; + python|cpp|typescript|full) + exec_with_env bash "/opt/vdm-build/${VARIANT}.sh" + ;; +esac project_cmd exec "$BUILD_NAME" \ --env "PLATFORM_VERSION=$PLATFORM_VERSION" \ diff --git a/scripts/ci/check-incus-runner.sh b/scripts/ci/check-incus-runner.sh new file mode 100755 index 0000000..01ccdb1 --- /dev/null +++ b/scripts/ci/check-incus-runner.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/../lib/common.sh" + +for command in incus jq tar zstd sha256sum; do + require_command "$command" +done + +[ "$(uname -s)" = Linux ] || die "The image runner must be Linux." +[ -r /dev/kvm ] && [ -w /dev/kvm ] || die "The image runner requires read/write access to /dev/kvm." + +incus_cmd info >/dev/null 2>&1 || die "The runner cannot communicate with the Incus daemon." + +storage_pool="${INCUS_STORAGE_POOL:-default}" +incus_cmd storage show "$storage_pool" >/dev/null 2>&1 || \ + die "Incus storage pool not found: $storage_pool" + +available_kib="$(df --output=avail -k "$ROOT_DIR" | awk 'NR == 2 {print $1}')" +required_kib="$((MIN_BUILD_FREE_GIB * 1024 * 1024))" +if [ "$available_kib" -lt "$required_kib" ]; then + available_gib="$((available_kib / 1024 / 1024))" + die "Insufficient build disk: ${available_gib} GiB available; ${MIN_BUILD_FREE_GIB} GiB required." +fi + +log "Incus runner preflight passed" diff --git a/scripts/configure-models.sh b/scripts/configure-models.sh index 6723d79..3d8b674 100755 --- a/scripts/configure-models.sh +++ b/scripts/configure-models.sh @@ -15,6 +15,8 @@ for agent in "${agents[@]}"; do die "Invalid model identifier: $model" fi + # The single-quoted program is evaluated inside the guest, where the injected variables exist. + # shellcheck disable=SC2016 project_cmd exec "$NAME" --env "AGENT_NAME=$agent" --env "AGENT_MODEL=$model" -- bash -c ' set -Eeuo pipefail file="/home/opencode/.config/opencode/agents/${AGENT_NAME}.md" diff --git a/scripts/create-runtime-env.sh b/scripts/create-runtime-env.sh index ba83195..9ce8f49 100755 --- a/scripts/create-runtime-env.sh +++ b/scripts/create-runtime-env.sh @@ -21,10 +21,6 @@ GITEA_MCP_URL= GITEA_MCP_TOKEN= NEXTCLOUD_MCP_URL= NEXTCLOUD_MCP_TOKEN= -JOOMLA_MCP_URL= -JOOMLA_MCP_TOKEN= -JCB_MCP_URL= -JCB_MCP_TOKEN= STT_MCP_URL= STT_MCP_TOKEN= VDM_LOCAL_LLM_BASE_URL=http://10.248.18.1:11434/v1 diff --git a/scripts/import-image-package.sh b/scripts/import-image-package.sh new file mode 100755 index 0000000..7a9ef97 --- /dev/null +++ b/scripts/import-image-package.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/lib/common.sh" + +PACKAGE_DIR="${1:-}" +REQUESTED_ALIAS="${2:-}" +[ -d "$PACKAGE_DIR" ] || die "Usage: $0 PACKAGE_DIRECTORY [IMAGE_ALIAS]" +[ -f "$PACKAGE_DIR/manifest.json" ] || die "Package manifest is missing." +[ -f "$PACKAGE_DIR/SHA256SUMS" ] || die "Package checksums are missing." + +( + cd "$PACKAGE_DIR" + sha256sum --check --strict SHA256SUMS +) + +jq -e ' + .schema == 1 and + (.platform | type == "string") and + (.version | type == "string") and + (.variant | type == "string") and + (.payload | type == "array" and length > 0) +' "$PACKAGE_DIR/manifest.json" >/dev/null || die "Package manifest is invalid." + +mapfile -t payload_names < <(jq -r '.payload[].name' "$PACKAGE_DIR/manifest.json") +metadata_files=() +data_files=() +for name in "${payload_names[@]}"; do + [[ "$name" != */* && "$name" != .* ]] || die "Unsafe payload name: $name" + [ -f "$PACKAGE_DIR/$name" ] || die "Package payload is missing: $name" + if tar -tf "$PACKAGE_DIR/$name" 2>/dev/null | grep -Eq '^(\./)?metadata\.yaml$'; then + metadata_files+=("$PACKAGE_DIR/$name") + else + data_files+=("$PACKAGE_DIR/$name") + fi +done +[ "${#metadata_files[@]}" -le 1 ] || die "Package contains more than one metadata archive." +payload_files=("${metadata_files[@]}" "${data_files[@]}") + +alias_name="${REQUESTED_ALIAS:-$(jq -r '.image' "$PACKAGE_DIR/manifest.json")}" +[ -n "$alias_name" ] && [ "$alias_name" != null ] || die "No image alias was supplied or recorded." +project_cmd image show "$alias_name" >/dev/null 2>&1 && die "Image alias already exists: $alias_name" + +project_cmd image import "${payload_files[@]}" --alias "$alias_name" +project_cmd image set-property "$alias_name" org.vdm.platform "$(jq -r '.platform' "$PACKAGE_DIR/manifest.json")" +project_cmd image set-property "$alias_name" org.vdm.version "$(jq -r '.version' "$PACKAGE_DIR/manifest.json")" +project_cmd image set-property "$alias_name" org.vdm.variant "$(jq -r '.variant' "$PACKAGE_DIR/manifest.json")" + +log "Imported $alias_name" diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index aca12d4..f59fbcd 100755 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -7,6 +7,13 @@ source "$ROOT_DIR/manifest/platform.env" # shellcheck disable=SC1091 source "$ROOT_DIR/manifest/toolchain.env" +if [ -f "$ROOT_DIR/.env" ]; then + set -a + # shellcheck disable=SC1091 + source "$ROOT_DIR/.env" + set +a +fi + log() { printf '\n\033[1;34m==> %s\033[0m\n' "$*" } @@ -38,8 +45,8 @@ project_cmd() { wait_for_vm() { local name="$1" - local attempt - for attempt in $(seq 1 180); do + local _ + for _ in $(seq 1 180); do if project_cmd exec "$name" --mode=non-interactive -- true >/dev/null 2>&1; then return 0 fi diff --git a/scripts/mcp-toggle.sh b/scripts/mcp-toggle.sh index 447e3de..c50c12c 100755 --- a/scripts/mcp-toggle.sh +++ b/scripts/mcp-toggle.sh @@ -10,7 +10,8 @@ STATE="${3:-}" [ -n "$NAME" ] && [ -n "$SERVER" ] && [ -n "$STATE" ] || die "Usage: $0 INSTANCE SERVER true|false" case "$STATE" in true|false) ;; *) die "State must be true or false" ;; esac -CONFIG="$AGENT_HOME/.config/opencode/opencode.json" +# The single-quoted program is evaluated inside the guest, where the injected variables exist. +# shellcheck disable=SC2016 project_cmd exec "$NAME" --env "MCP_SERVER=$SERVER" --env "MCP_STATE=$STATE" -- bash -c ' set -Eeuo pipefail config="/home/opencode/.config/opencode/opencode.json" diff --git a/scripts/occtl.sh b/scripts/occtl.sh index e8a559c..c17c665 100755 --- a/scripts/occtl.sh +++ b/scripts/occtl.sh @@ -17,6 +17,7 @@ case "$command" in mcp) exec "$SCRIPT_DIR/mcp-toggle.sh" "$@" ;; models) exec "$SCRIPT_DIR/configure-models.sh" "$@" ;; package) exec "$SCRIPT_DIR/package-image.sh" "$@" ;; + import-package) exec "$SCRIPT_DIR/import-image-package.sh" "$@" ;; publish-package) exec "$SCRIPT_DIR/publish-gitea-package.sh" "$@" ;; export) exec "$SCRIPT_DIR/export-instance.sh" "$@" ;; import) exec "$SCRIPT_DIR/import-instance.sh" "$@" ;; @@ -35,6 +36,7 @@ occtl commands: mcp INSTANCE SERVER true|false models INSTANCE package VARIANT + import-package DIRECTORY [IMAGE_ALIAS] publish-package VARIANT export INSTANCE [FILE] import FILE INSTANCE diff --git a/scripts/package-image.sh b/scripts/package-image.sh index 7ead694..dcada41 100755 --- a/scripts/package-image.sh +++ b/scripts/package-image.sh @@ -9,21 +9,56 @@ OUTPUT_DIR="${2:-$ROOT_DIR/build/packages/${PLATFORM_VERSION}/${VARIANT}}" variant_exists "$VARIANT" || die "Unknown variant: $VARIANT" ALIAS="$(image_alias "$VARIANT")" -rm -rf "$OUTPUT_DIR" install -d -m 0750 "$OUTPUT_DIR" +find "$OUTPUT_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + project_cmd image export "$ALIAS" "$OUTPUT_DIR/${INCUS_IMAGE_PREFIX}-${VARIANT}-${PLATFORM_VERSION}" -( - cd "$OUTPUT_DIR" - sha256sum ./* > SHA256SUMS -) +source_revision="unknown" +if command -v git >/dev/null 2>&1 && git -C "$ROOT_DIR" rev-parse --verify HEAD >/dev/null 2>&1; then + source_revision="$(git -C "$ROOT_DIR" rev-parse HEAD)" +fi + +payload_json="$( + find "$OUTPUT_DIR" -maxdepth 1 -type f -printf '%f\n' | LC_ALL=C sort | while IFS= read -r name; do + jq -n \ + --arg name "$name" \ + --arg sha256 "$(sha256sum "$OUTPUT_DIR/$name" | awk '{print $1}')" \ + --argjson bytes "$(stat -c '%s' "$OUTPUT_DIR/$name")" \ + '{name:$name,sha256:$sha256,bytes:$bytes}' + done | jq -s . +)" jq -n \ + --argjson schema 1 \ --arg platform "$PLATFORM_NAME" \ --arg version "$PLATFORM_VERSION" \ --arg variant "$VARIANT" \ --arg image "$ALIAS" \ - '{platform:$platform,version:$version,variant:$variant,image:$image}' \ + --arg architecture "$(uname -m)" \ + --arg source_revision "$source_revision" \ + --arg built_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --argjson payload "$payload_json" \ + '{ + schema:$schema, + platform:$platform, + version:$version, + variant:$variant, + image:$image, + architecture:$architecture, + source_revision:$source_revision, + packaged_at:$built_at, + payload:$payload + }' \ > "$OUTPUT_DIR/manifest.json" +checksums_file="$(mktemp)" +trap 'rm -f "$checksums_file"' EXIT +( + cd "$OUTPUT_DIR" + find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%P\0' \ + | LC_ALL=C sort -z \ + | xargs -0 sha256sum > "$checksums_file" +) +install -m 0640 "$checksums_file" "$OUTPUT_DIR/SHA256SUMS" + log "Packaged image in $OUTPUT_DIR" diff --git a/scripts/publish-gitea-package.sh b/scripts/publish-gitea-package.sh index cc0258a..0294fe6 100755 --- a/scripts/publish-gitea-package.sh +++ b/scripts/publish-gitea-package.sh @@ -17,7 +17,10 @@ PACKAGE_NAME="${GITEA_PACKAGE_NAME:-vdm-opencode-${VARIANT}}" for file in "$PACKAGE_DIR"/*; do [ -f "$file" ] || continue name="$(basename "$file")" - curl --fail-with-body \ + curl --fail-with-body --silent --show-error \ + --retry 4 \ + --retry-all-errors \ + --connect-timeout 15 \ --user "${GITEA_PACKAGE_USER}:${GITEA_PACKAGE_TOKEN}" \ --upload-file "$file" \ "${GITEA_BASE_URL}/api/packages/${GITEA_PACKAGE_OWNER}/generic/${PACKAGE_NAME}/${PLATFORM_VERSION}/${name}" diff --git a/scripts/verify-image.sh b/scripts/verify-image.sh index 71fb037..ef17dd1 100755 --- a/scripts/verify-image.sh +++ b/scripts/verify-image.sh @@ -7,18 +7,54 @@ source "$SCRIPT_DIR/lib/common.sh" INSTANCE="${1:?instance required}" VARIANT="${2:?variant required}" +guest_output() { + project_cmd exec "$INSTANCE" --mode=non-interactive -- "$@" | tr -d '\r' +} + +assert_version() { + local label="$1" + local expected="$2" + local actual="$3" + [ "$actual" = "$expected" ] || die "$label version mismatch: expected $expected, got $actual" +} + project_cmd exec "$INSTANCE" -- test -x "/home/$AGENT_USER/.local/bin/opencode" project_cmd exec "$INSTANCE" -- test -f /etc/opencode/opencode.json project_cmd exec "$INSTANCE" -- test -f /etc/vdm-opencode-platform/build.json +# The jq program must reach the guest unchanged. +# shellcheck disable=SC2016 project_cmd exec "$INSTANCE" -- jq -e --arg variant "$VARIANT" '.variant == $variant' /etc/vdm-opencode-platform/build.json >/dev/null project_cmd exec "$INSTANCE" -- test ! -e "/home/$AGENT_USER/.local/share/opencode/auth.json" project_cmd exec "$INSTANCE" -- test ! -e "/home/$AGENT_USER/.local/share/opencode/mcp-auth.json" project_cmd exec "$INSTANCE" -- bash -c \ '! find /home/opencode -xdev -type f \( -name "id_rsa*" -o -name "id_ed25519*" -o -name ".git-credentials" \) -print -quit | grep -q .' +assert_version \ + OpenCode \ + "$OPENCODE_EXPECTED_VERSION" \ + "$(guest_output "/home/$AGENT_USER/.local/bin/opencode" --version | tr -d '[:space:]')" + +assert_version \ + "Git MCP" \ + "$GIT_MCP_EXPECTED_VERSION" \ + "$(guest_output "/home/$AGENT_USER/.local/share/uv/tools/mcp-server-git/bin/python" -c 'import importlib.metadata; print(importlib.metadata.version("mcp-server-git"))' | tr -d '[:space:]')" + if [ "$VARIANT" = php ] || [ "$VARIANT" = typescript ] || [ "$VARIANT" = full ]; then - project_cmd exec "$INSTANCE" -- node --version - project_cmd exec "$INSTANCE" -- npx playwright --version + assert_version \ + Playwright \ + "$PLAYWRIGHT_EXPECTED_VERSION" \ + "$(guest_output playwright --version | awk '{print $2}')" + assert_version \ + "Playwright MCP" \ + "$PLAYWRIGHT_MCP_EXPECTED_VERSION" \ + "$(guest_output npm list --global --json --depth=0 | jq -r '.dependencies["@playwright/mcp"].version')" + project_cmd exec "$INSTANCE" -- test -x /usr/local/bin/vdm-playwright-mcp + project_cmd exec "$INSTANCE" -- test -d /opt/ms-playwright +fi + +if [ "$VARIANT" = python ] || [ "$VARIANT" = full ]; then + assert_version Ruff "$RUFF_EXPECTED_VERSION" "$(guest_output "/home/$AGENT_USER/.local/bin/ruff" --version | awk '{print $2}')" + assert_version mypy "$MYPY_EXPECTED_VERSION" "$(guest_output "/home/$AGENT_USER/.local/bin/mypy" --version | awk '{print $2}')" fi log "Image verification passed for $VARIANT" diff --git a/tests/browser-smoke.sh b/tests/browser-smoke.sh index 6694107..85772ad 100755 --- a/tests/browser-smoke.sh +++ b/tests/browser-smoke.sh @@ -2,12 +2,14 @@ set -Eeuo pipefail command -v node >/dev/null -npx playwright --version +command -v playwright >/dev/null +export PLAYWRIGHT_BROWSERS_PATH="${PLAYWRIGHT_BROWSERS_PATH:-/opt/ms-playwright}" +playwright --version output="$(mktemp --suffix=.png)" trap 'rm -f "$output"' EXIT -npx playwright screenshot \ +playwright screenshot \ --browser=chromium \ 'data:text/html,

VDM%20browser%20smoke%20test

' \ "$output" diff --git a/tests/fixtures/incus b/tests/fixtures/incus new file mode 100755 index 0000000..12ec7ac --- /dev/null +++ b/tests/fixtures/incus @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +if [ "${1:-}" = info ]; then + exit 0 +fi + +if [ "${1:-}" = --project ]; then + shift 2 +fi + +case "${1:-}:${2:-}" in + image:export) + output="${4:?mock export output is required}" + metadata_dir="$(mktemp -d)" + trap 'rm -rf "$metadata_dir"' EXIT + printf 'architecture: x86_64\ncreation_date: 0\n' > "$metadata_dir/metadata.yaml" + tar -C "$metadata_dir" -cf "${output}.tar" metadata.yaml + dd if=/dev/zero of="${output}.rootfs" bs=1024 count=1 status=none + ;; + image:show) + exit 1 + ;; + image:import|image:set-property) + printf '%s\n' "$*" >> "${MOCK_INCUS_LOG:?MOCK_INCUS_LOG is required}" + ;; + *) + printf 'Unexpected mock Incus invocation: %s\n' "$*" >&2 + exit 1 + ;; +esac diff --git a/tests/package-roundtrip.sh b/tests/package-roundtrip.sh new file mode 100755 index 0000000..96b000b --- /dev/null +++ b/tests/package-roundtrip.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +export PATH="$ROOT_DIR/tests/fixtures:$PATH" +export MOCK_INCUS_LOG="$tmp_dir/incus.log" +output_dir="$tmp_dir/package" + +"$ROOT_DIR/scripts/package-image.sh" base "$output_dir" + +jq -e ' + .schema == 1 and + .platform == "vdm-opencode-platform" and + .version == "0.2.0" and + .variant == "base" and + (.payload | length == 2) +' "$output_dir/manifest.json" >/dev/null + +( + cd "$output_dir" + sha256sum --check --strict SHA256SUMS +) + +"$ROOT_DIR/scripts/import-image-package.sh" "$output_dir" "vdm-opencode-base/test" +grep -Eq 'image import .*\.tar .*\.rootfs --alias vdm-opencode-base/test' "$MOCK_INCUS_LOG" + +printf 'Image package round-trip test passed.\n' diff --git a/tests/validate-repository.sh b/tests/validate-repository.sh index c044b83..4b5ed7f 100755 --- a/tests/validate-repository.sh +++ b/tests/validate-repository.sh @@ -4,22 +4,53 @@ ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" fail=0 +find_args=("$ROOT_DIR" -type f -not -path "$ROOT_DIR/.git/*" -not -path "$ROOT_DIR/build/*") + while IFS= read -r -d '' script; do bash -n "$script" || fail=1 -done < <(find "$ROOT_DIR" -type f -name '*.sh' -print0) +done < <(find "${find_args[@]}" -name '*.sh' -print0) if command -v shellcheck >/dev/null 2>&1; then - mapfile -d '' scripts < <(find "$ROOT_DIR" -type f -name '*.sh' -print0) - shellcheck -x "${scripts[@]}" || fail=1 + mapfile -d '' scripts < <(find "${find_args[@]}" -name '*.sh' -print0) + if ((${#scripts[@]} > 0)); then + shellcheck -x "${scripts[@]}" || fail=1 + fi fi while IFS= read -r -d '' json; do jq empty "$json" || fail=1 -done < <(find "$ROOT_DIR" -type f -name '*.json' -print0) +done < <(find "${find_args[@]}" -name '*.json' -print0) if command -v yamllint >/dev/null 2>&1; then + yaml_paths=() + for path in incus manifest broker .github .gitea; do + [ -e "$ROOT_DIR/$path" ] && yaml_paths+=("$ROOT_DIR/$path") + done yamllint -d '{extends: default, rules: {line-length: disable, truthy: disable, document-start: disable}}' \ - "$ROOT_DIR/incus" "$ROOT_DIR/manifest" "$ROOT_DIR/broker" "$ROOT_DIR/.gitea" || fail=1 + "${yaml_paths[@]}" || fail=1 +fi + +if grep -RIE --exclude-dir=.git --exclude-dir=build --exclude=validate-repository.sh \ + '(nikosdion/joomla-mcp-php|OnepointConsultingLtd/joomla-mcp-server|joomla_mcp4joomla|joomla_component_mcp)' \ + "$ROOT_DIR"; then + printf 'A non-VDM Joomla MCP reference is still present.\n' >&2 + fail=1 +fi + +if grep -RIE --exclude-dir=.git --exclude-dir=build --exclude='*.md' --exclude=validate-repository.sh \ + '(@latest|REVIEW_AND_PIN)' "$ROOT_DIR"; then + printf 'A floating production dependency is still present.\n' >&2 + fail=1 +fi + +if grep -RIE '\$\{\{[[:space:]]*gitea\.' "$ROOT_DIR/.github/workflows"; then + printf 'A GitHub workflow contains a Gitea-only expression context.\n' >&2 + fail=1 +fi + +if grep -RIE '\$\{\{[[:space:]]*github\.' "$ROOT_DIR/.gitea/workflows"; then + printf 'A Gitea workflow contains a GitHub-only expression context.\n' >&2 + fail=1 fi if grep -RIE \ @@ -33,7 +64,7 @@ if grep -RIE \ fail=1 fi -if find "$ROOT_DIR" -type f \( -name 'auth.json' -o -name 'mcp-auth.json' -o -name '.git-credentials' \) | grep -q .; then +if find "${find_args[@]}" \( -name 'auth.json' -o -name 'mcp-auth.json' -o -name '.git-credentials' \) | grep -q .; then printf 'Forbidden credential-state filename detected.\n' >&2 fail=1 fi From acce7bcdead49c26e01626c678170ef410bb1f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eW=C9=98yn?= <5607939+Llewellynvdm@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:02:12 +0200 Subject: [PATCH 2/2] Make validation compatible with hosted ShellCheck --- scripts/ci/check-incus-runner.sh | 4 +++- scripts/import-image-package.sh | 8 ++++++-- scripts/import-instance.sh | 4 +++- scripts/mcp-toggle.sh | 4 +++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/ci/check-incus-runner.sh b/scripts/ci/check-incus-runner.sh index 01ccdb1..744afb4 100755 --- a/scripts/ci/check-incus-runner.sh +++ b/scripts/ci/check-incus-runner.sh @@ -10,7 +10,9 @@ for command in incus jq tar zstd sha256sum; do done [ "$(uname -s)" = Linux ] || die "The image runner must be Linux." -[ -r /dev/kvm ] && [ -w /dev/kvm ] || die "The image runner requires read/write access to /dev/kvm." +if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then + die "The image runner requires read/write access to /dev/kvm." +fi incus_cmd info >/dev/null 2>&1 || die "The runner cannot communicate with the Incus daemon." diff --git a/scripts/import-image-package.sh b/scripts/import-image-package.sh index 7a9ef97..6614294 100755 --- a/scripts/import-image-package.sh +++ b/scripts/import-image-package.sh @@ -40,8 +40,12 @@ done payload_files=("${metadata_files[@]}" "${data_files[@]}") alias_name="${REQUESTED_ALIAS:-$(jq -r '.image' "$PACKAGE_DIR/manifest.json")}" -[ -n "$alias_name" ] && [ "$alias_name" != null ] || die "No image alias was supplied or recorded." -project_cmd image show "$alias_name" >/dev/null 2>&1 && die "Image alias already exists: $alias_name" +if [ -z "$alias_name" ] || [ "$alias_name" = null ]; then + die "No image alias was supplied or recorded." +fi +if project_cmd image show "$alias_name" >/dev/null 2>&1; then + die "Image alias already exists: $alias_name" +fi project_cmd image import "${payload_files[@]}" --alias "$alias_name" project_cmd image set-property "$alias_name" org.vdm.platform "$(jq -r '.platform' "$PACKAGE_DIR/manifest.json")" diff --git a/scripts/import-instance.sh b/scripts/import-instance.sh index 0c288a8..3cc6ed4 100755 --- a/scripts/import-instance.sh +++ b/scripts/import-instance.sh @@ -6,7 +6,9 @@ source "$SCRIPT_DIR/lib/common.sh" BACKUP="${1:-}" NAME="${2:-}" -[ -f "$BACKUP" ] && [ -n "$NAME" ] || die "Usage: $0 BACKUP.tar.zst NEW_INSTANCE_NAME" +if [ ! -f "$BACKUP" ] || [ -z "$NAME" ]; then + die "Usage: $0 BACKUP.tar.zst NEW_INSTANCE_NAME" +fi project_cmd import "$BACKUP" "$NAME" log "Imported $NAME" diff --git a/scripts/mcp-toggle.sh b/scripts/mcp-toggle.sh index c50c12c..8ad3e7f 100755 --- a/scripts/mcp-toggle.sh +++ b/scripts/mcp-toggle.sh @@ -7,7 +7,9 @@ source "$SCRIPT_DIR/lib/common.sh" NAME="${1:-}" SERVER="${2:-}" STATE="${3:-}" -[ -n "$NAME" ] && [ -n "$SERVER" ] && [ -n "$STATE" ] || die "Usage: $0 INSTANCE SERVER true|false" +if [ -z "$NAME" ] || [ -z "$SERVER" ] || [ -z "$STATE" ]; then + die "Usage: $0 INSTANCE SERVER true|false" +fi case "$STATE" in true|false) ;; *) die "State must be true or false" ;; esac # The single-quoted program is evaluated inside the guest, where the injected variables exist.