Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions .env.example
100644 → 100755
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
45 changes: 45 additions & 0 deletions .gitea/workflows/build-images.yaml
Original file line number Diff line number Diff line change
@@ -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 }}"
27 changes: 27 additions & 0 deletions .gitea/workflows/validate.yaml
Original file line number Diff line number Diff line change
@@ -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
12 changes: 7 additions & 5 deletions .github/workflows/build-images.yaml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand All @@ -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 }}/
Expand All @@ -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 }}
Expand Down
23 changes: 20 additions & 3 deletions .github/workflows/validate.yaml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 12 additions & 3 deletions CHANGELOG.md
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# 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.
- Added Incus project, networks, ACL and six VM profiles.
- 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.
67 changes: 39 additions & 28 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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

Expand All @@ -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`
2 changes: 1 addition & 1 deletion VERSION
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0
0.2.0
2 changes: 1 addition & 1 deletion docs/credentials-and-brokers.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 9 additions & 2 deletions docs/gitea-packages.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading