diff --git a/.env.example b/.env.example old mode 100644 new mode 100755 index 5c68511..237f47c --- 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 100755 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 100755 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 old mode 100644 new mode 100755 index 305b6d1..96b1479 --- a/.github/workflows/build-images.yaml +++ b/.github/workflows/build-images.yaml @@ -16,20 +16,21 @@ concurrency: jobs: build: runs-on: [self-hosted, linux, incus] - timeout-minutes: 180 + timeout-minutes: 240 strategy: fail-fast: false max-parallel: 1 matrix: variant: [base, php, python, cpp, typescript, full] steps: - - uses: actions/checkout@v4 + - 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 }}" @@ -38,7 +39,7 @@ jobs: run: ./scripts/package-image.sh "${{ matrix.variant }}" - name: Upload downloadable image package - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: vdm-opencode-${{ matrix.variant }}-${{ github.ref_name }} path: build/packages/*/${{ matrix.variant }}/ @@ -49,6 +50,7 @@ jobs: - name: Publish Gitea Generic Package 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 old mode 100644 new mode 100755 index 4d7157d..5c12f25 --- 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 old mode 100644 new mode 100755 index d086b13..f2cf63d --- 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,7 +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, VDM Joomla, future JCB and speech MCP definitions. -- Added portable GitHub image artifacts while retaining opt-in Gitea Generic Package publishing. -- Removed third-party Joomla MCP candidates. +- 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 old mode 100644 new mode 100755 index 97a5fac..09937e7 --- a/README.md +++ b/README.md @@ -2,23 +2,27 @@ A company-wide, version-controlled platform definition for hardened OpenCode agent virtual machines on Incus. -The repository is the authority. Incus images and package registries contain generated artifacts. +The repository is the authority. Incus images and Gitea packages are generated artifacts. ## What this repository provides -- A common Ubuntu 24.04 VM base and five variants: `base`, `php`, `python`, `cpp`, `typescript`, and `full`. -- Repeatable Incus projects, networks, ACL scaffolding, profiles, image builds and instance launches. +- A common Ubuntu 24.04 VM base and five image variants: `base`, `php`, `python`, `cpp`, `typescript`, and `full`. +- Repeatable Incus projects, networks, ACL scaffolding, profiles, resource limits, image builds and launches. - 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, the future VDM Joomla MCP, future JCB - MCP, 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. -- Gitea Generic Package publishing and GitHub Actions artifact downloads. +- A reference external broker stack for OpenBao, an LLM gateway and a TLS reverse proxy. +- 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. ## Recommended image strategy +Use specialised images for normal work and the full image only when a project genuinely crosses languages. + | Image | Primary use | Browser MCP | |---|---|---| | `base` | Repository analysis, documentation, light automation | Disabled | @@ -28,44 +32,48 @@ The repository is the authority. Incus images and package registries contain gen | `typescript` | Web applications, Node.js and browser automation | Enabled | | `full` | Mixed-language platform work | Enabled | -Use specialised images for ordinary work and `full` only for genuinely mixed-language projects. +Every image inherits the same security policy and agent framework. ## First deployment ```bash cp .env.example .env +# 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 ``` -Do not put secrets in `.env`. Runtime tokens belong in a mode-`0600` session file under -`/run/user/$UID/vdm-opencode/`. - -## Build and distribution +Inside OpenCode, use `/connect` for ChatGPT Plus where supported by OpenCode. Anthropic subscription reuse is not +configured: use an approved Anthropic API credential or the company LLM gateway. Grok should use the xAI API or +company gateway. Local Llama uses the configured OpenAI-compatible local endpoint. -The image workflow runs on a self-hosted runner labelled `self-hosted`, `linux`, and `incus`. Manual runs and -version tags build all variants and upload downloadable GitHub Actions artifacts. Tagged builds can additionally -publish the same files to Gitea Generic Packages when the Gitea publishing variables and secret are configured. +## Credentials -See `docs/github-gitea-roadmap.md` for the hosting choices and migration path. +No long-lived credential belongs in an image. -## MCP policy +`start-session.sh` creates a root-owned guest runtime directory under `/run/vdm-opencode-session`, sets +`XDG_DATA_HOME` to that tmpfs location, injects only the current short-lived variables, launches OpenCode, and +deletes the directory on exit. OpenCode provider and MCP OAuth material generated during that session therefore +does not survive a clean VM stop. -- Gitea MCP remains a supported, disabled-by-default first-party integration. -- `https://github.com/vast-development-method/joomla-mcp` is the only approved Joomla MCP source and remains - disabled until its first reviewed release. -- JCB MCP remains deferred and disabled until the internal implementation has a repository and approved release. -- No long-lived MCP credential belongs in an image. +For production, point the VM at a trusted external LLM/MCP gateway and issue short-lived, scoped session tokens. -## Release limitations +## Important limitations -The workflow and repository structure are ready for repeatable validation and image packaging, but production -promotion remains blocked until every `REVIEW_AND_PIN` and floating `@latest` reference is replaced with an -approved immutable version and checksum. See `docs/versioning-and-promotion.md`. +- 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 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. +- Incus packages are architecture-specific and must be produced by a trusted hardware-virtualisation runner. ## Documentation @@ -76,7 +84,10 @@ Start with: - `docs/security-model.md` - `docs/image-variants.md` - `docs/mcp-catalog.md` -- `docs/github-gitea-roadmap.md` +- `docs/provider-integration.md` +- `docs/browser-testing.md` +- `docs/credentials-and-brokers.md` +- `docs/scaling-and-operations.md` - `docs/gitea-packages.md` -- `docs/versioning-and-promotion.md` -- `docs/known-limitations.md` +- `docs/github-and-gitea.md` +- `docs/backup-and-migration.md` diff --git a/VERSION b/VERSION old mode 100644 new mode 100755 index 6e8bf73..0ea3a94 --- 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 old mode 100644 new mode 100755 index 20b7d5a..9897e24 --- 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 old mode 100644 new mode 100755 index de22a8c..7b81e81 --- 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 100755 index 0000000..4107f57 --- /dev/null +++ b/docs/github-and-gitea.md @@ -0,0 +1,58 @@ +# 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. + +Protect `master` and require the validation workflow. For the first release candidate, manually run the image +workflow, download all six packages, verify `SHA256SUMS` in each one and import at least one package into a clean +Incus project. Tags are release identities and must never be recreated or force-moved after publication. + +## Production promotion gate + +A successful workflow is necessary but not sufficient for production promotion. Promote only from a clean trusted +runner after validation, the complete six-variant build, checksum verification, guest and security tests, a clean +pilot import and a matching protected release tag have all passed. + +## 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/github-gitea-roadmap.md b/docs/github-gitea-roadmap.md deleted file mode 100644 index 90b3445..0000000 --- a/docs/github-gitea-roadmap.md +++ /dev/null @@ -1,65 +0,0 @@ -# GitHub and Gitea operating roadmap - -## Recommended immediate model - -Use GitHub as the current source and review authority while keeping Gitea as an artifact destination and future -source mirror. - -- GitHub hosts `master`, pull requests and workflow definitions. -- A self-hosted runner with Incus and hardware virtualisation builds images. -- GitHub Actions artifacts provide immediate downloadable packages for each image variant. -- Gitea Generic Packages remain the durable internal package authority when explicitly enabled. -- Gitea MCP remains available to agent VMs through the trusted MCP gateway. - -This model requires no immediate repository move and preserves the Gitea investment. - -## Required GitHub setup - -1. Register a dedicated self-hosted Linux runner on a trusted Incus host. -2. Add runner labels `linux` and `incus`. -3. Give the runner account passwordless access only to the Incus operations required by the build scripts. -4. Protect `master` and require the validation workflow. -5. Run `Build Incus images` manually for the first verification. -6. Confirm all six artifact archives download and their `SHA256SUMS` files verify. -7. For tagged Gitea publishing, configure: - - repository variable `GITEA_PUBLISH_ENABLED=true`; - - repository variable `GITEA_PACKAGE_OWNER`; - - optional repository variable `GITEA_PACKAGE_USER`; - - repository secret `GITEA_PACKAGE_TOKEN`. -8. Use a Gitea token limited to package writes for the intended owner. - -Without `GITEA_PUBLISH_ENABLED=true`, GitHub builds and artifacts still succeed and no Gitea write is attempted. - -## Moving the source authority back to Gitea - -When Gitea is ready to become authoritative: - -1. Create a protected Gitea repository with `master` as its default branch. -2. Mirror GitHub into Gitea and compare branch and tag SHAs. -3. Install a Gitea Actions runner on the same trusted Incus builder. -4. Copy the workflows into Gitea's supported workflow directory if the deployed Gitea version does not consume - `.github/workflows` directly. -5. Replace GitHub artifact upload steps with Gitea artifact support or keep Generic Packages as the only durable - output. -6. Store package variables and secrets in Gitea Actions. -7. Run validation and a complete six-variant image build from Gitea. -8. Verify package checksums and import one image into a clean Incus project. -9. Change developer remotes only after the Gitea build and restore tests pass. -10. Keep GitHub as a read-only mirror if public visibility, external collaboration or disaster recovery is useful. - -## Dual-host operation - -Avoid two writable authorities. Choose one push authority and mirror it one-way: - -```text -authoritative Git host -> read-only mirror -authoritative Git host -> CI runner -> GitHub artifacts and/or Gitea packages -``` - -Tags are release identities. Never recreate or force-move a published tag on either host. - -## Promotion gate - -A successful build is not by itself a production promotion. Production requires immutable upstream versions, -checksums, a clean runner, guest and security tests, artifact verification, a pilot import, and a matching protected -release tag. diff --git a/docs/jcb-mcp-roadmap.md b/docs/jcb-mcp-roadmap.md old mode 100644 new mode 100755 index 0125369..db1458f --- 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 old mode 100644 new mode 100755 index cdb2dc2..ea84a1f --- a/docs/joomla-mcp-study.md +++ b/docs/joomla-mcp-study.md @@ -1,29 +1,26 @@ -# VDM Joomla MCP integration +# Joomla MCP integration gate -The only approved Joomla MCP implementation for this platform is: +Reviewed: 2026-07-22 -- Repository: `https://github.com/vast-development-method/joomla-mcp` -- Owner: Vast Development Method -- Status: not yet released -- Platform state: disabled +The platform will use only the VDM-owned Joomla MCP: -No third-party Joomla MCP implementation is selected, installed, referenced as a candidate or permitted as a +`https://github.com/vast-development-method/joomla-mcp` + +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. ## Activation gate -The existing remote `joomla` configuration entry is an inert compatibility hook. Do not enable it until the VDM -repository exists and supplies all of the following: - -1. A tagged release and immutable source reference. -2. Documented transport, authentication and Joomla compatibility. -3. A reviewed tool inventory and Joomla ACL mapping. -4. Read-only defaults and explicit destructive-operation policy. -5. Audit logging with credential and sensitive-parameter redaction. -6. Pagination, response-size and rate limits. -7. Automated tests for the supported Joomla 6.x versions. -8. Signed or checksummed release artifacts. -9. Upgrade, rollback and incident-response procedures. - -When those gates pass, pin the exact release in `manifest/sources.lock.yaml`, set the runtime -`JOOMLA_MCP_URL` and short-lived `JOOMLA_MCP_TOKEN`, then enable the server only for agents that need it. +Add the integration only when all of the following are true: + +- 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. + +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 old mode 100644 new mode 100755 index 87d4303..06df2dd --- a/docs/known-limitations.md +++ b/docs/known-limitations.md @@ -2,12 +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. -- The VDM Joomla MCP is not yet released and its generic platform hook remains disabled. -- 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. -- Image builds require a self-hosted runner with Incus access and hardware virtualization on either GitHub or Gitea. -- GitHub Actions artifacts are retained for 30 days; use Gitea Generic Packages for durable internal retention. +- 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 old mode 100644 new mode 100755 index 6e6dd7a..64acb21 --- a/docs/mcp-catalog.md +++ b/docs/mcp-catalog.md @@ -4,44 +4,47 @@ ### Git -Uses `mcp-server-git` from the Model Context Protocol project. It is constrained to `/workspace`. +Uses `mcp-server-git` from the Model Context Protocol project. It can inspect and modify repositories and is still +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 browser QA. -Use ordinary Playwright tests or the CLI for deterministic suites. +Microsoft's Playwright MCP is installed in PHP, TypeScript and full images. Keep it for exploratory, persistent +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 ### GitHub Use the first-party `github/github-mcp-server`, preferably through GitHub's remote endpoint or a company gateway. -Enable only the toolsets needed for a task. +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`. Gitea integration is a permanent platform requirement and must remain available even -while GitHub is used as the source host or CI front end. +Use Gitea's own `https://gitea.com/gitea/gitea-mcp`. The company authority defaults to `https://git.vdm.dev`. +The platform keeps its runtime endpoint and short-lived token support. Expose the audited deployment through the +company MCP gateway. -## Internal integrations awaiting releases +## Candidate requiring approval -### Joomla +### Nextcloud -The only approved Joomla MCP source is `https://github.com/vast-development-method/joomla-mcp`. -The generic remote configuration hook remains disabled until that repository publishes a reviewed release. -No third-party Joomla MCP is selected, documented, installed or permitted by this platform. +`cbcoutinho/nextcloud-mcp-server` is the current leading candidate because it exposes broad Files, Calendar, +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. -### JCB +## Deferred VDM integrations -JCB MCP support is deferred. The disabled generic hook remains solely to avoid an image-format migration later; -it must not be enabled until the VDM repository, protocol and first approved release exist. +### Joomla MCP -## Candidate requiring approval +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`. -### Nextcloud +### JCB MCP -The current community candidate remains disabled until code, dependency and permission review is complete. +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 old mode 100644 new mode 100755 index 06a223a..3ee7aa6 --- 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 old mode 100644 new mode 100755 index 2efe168..0e15913 --- 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 -- Publish and audit the first `vast-development-method/joomla-mcp` release. -- Pin and integrate only the VDM Joomla MCP implementation. +- 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 old mode 100644 new mode 100755 index a246342..b90b268 --- 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 old mode 100644 new mode 100755 index ef8d2c4..b04315e --- 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 old mode 100644 new mode 100755 index 160c6dd..449eeea --- 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 old mode 100644 new mode 100755 index c778e8f..46dbd4d --- a/manifest/mcp-catalog.yaml +++ b/manifest/mcp-catalog.yaml @@ -21,13 +21,13 @@ servers: transport: remote-or-stdio source: https://gitea.com/gitea/gitea-mcp default_authority: https://git.vdm.dev - note: Preserve this integration; pin an audited release before enabling it 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 + default_authority: operator-supplied note: Audit and pin before adoption. playwright: status: enabled-on-web-images @@ -36,21 +36,18 @@ servers: source: https://github.com/microsoft/playwright-mcp package: "@playwright/mcp" note: Prefer deterministic Playwright tests for suites; keep MCP for exploratory QA. - joomla: - status: internal-unfinished - trust: internal - transport: remote-preferred - source: https://github.com/vast-development-method/joomla-mcp - note: This is the only approved Joomla MCP source. Keep disabled until its first reviewed release. - jcb: - status: internal-unfinished - trust: internal - transport: remote-preferred - source: pending - note: Keep disabled until the VDM JCB MCP repository and first approved release are available. speech_to_text: status: optional trust: internal-endpoint transport: command-or-remote source: internal OpenAI-compatible transcription endpoint - note: Host microphone capture remains outside the VM; inject only transcribed text. + 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 old mode 100644 new mode 100755 index f469f8a..44efae7 --- 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 old mode 100644 new mode 100755 index 71553b9..932bb7c --- a/manifest/sources.lock.yaml +++ b/manifest/sources.lock.yaml @@ -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 +deferred_integrations: joomla_mcp: repository: https://github.com/vast-development-method/joomla-mcp build_ref: DISABLED_UNTIL_FIRST_RELEASE jcb_mcp: - repository: pending - build_ref: DISABLED_UNTIL_REPOSITORY_EXISTS + 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 old mode 100644 new mode 100755 index 991b45a..0572019 --- 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..744afb4 --- /dev/null +++ b/scripts/ci/check-incus-runner.sh @@ -0,0 +1,30 @@ +#!/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." +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." + +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..6614294 --- /dev/null +++ b/scripts/import-image-package.sh @@ -0,0 +1,55 @@ +#!/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")}" +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")" +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/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/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..8ad3e7f 100755 --- a/scripts/mcp-toggle.sh +++ b/scripts/mcp-toggle.sh @@ -7,10 +7,13 @@ 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 -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 4cf2c53..4b5ed7f 100755 --- a/tests/validate-repository.sh +++ b/tests/validate-repository.sh @@ -4,30 +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) - if (("${#scripts[@]}" > 0)); then + 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 - if (("${#yaml_paths[@]}" > 0)); then - yamllint -d '{extends: default, rules: {line-length: disable, truthy: disable, document-start: disable}}' \ - "${yaml_paths[@]}" || fail=1 - fi + yamllint -d '{extends: default, rules: {line-length: disable, truthy: disable, document-start: disable}}' \ + "${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 \ @@ -41,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