From 0be918d0b9f75ebe262c0da772c5ac5ef92f6742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yuniel=20Acosta=20P=C3=A9rez?= <33158051+yacosta738@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:13:57 +0100 Subject: [PATCH 1/2] feat(plugins): automate plugin release publishing on Cloudflare Add tag-driven plugin release automation with immutable artifact paths, signing, and catalog updates so plugin releases deploy consistently without per-plugin workflow changes. --- .github/workflows/README.md | 29 + .github/workflows/publish-plugins.yml | 614 +++++++++++++++--- .lycheeignore | 3 + .../plugins/memory-surreal-graphs/Cargo.toml | 13 + clients/agent-runtime/src/onboard/wizard.rs | 7 +- .../web/apps/docs/src/content/docs/en/404.mdx | 24 +- .../src/content/docs/en/guides/plugins.md | 49 +- .../apps/docs/src/content/docs/en/index.mdx | 18 +- .../web/apps/docs/src/content/docs/es/404.mdx | 24 +- .../src/content/docs/es/guides/plugins.md | 51 +- .../apps/docs/src/content/docs/es/index.mdx | 18 +- clients/web/apps/docs/src/pages/404.astro | 273 -------- clients/web/apps/plugins/README.md | 30 + clients/web/apps/plugins/public/_headers | 24 + .../0.1.0/memory.surreal.graphs.wasm | Bin 0 -> 99 bytes .../0.1.0/plugin-manifest.json | 24 + clients/web/apps/plugins/public/catalog.json | 27 +- .../web/apps/plugins/src/pages/index.astro | 14 + clients/web/apps/plugins/vercel.json | 38 -- 19 files changed, 802 insertions(+), 478 deletions(-) delete mode 100644 clients/web/apps/docs/src/pages/404.astro create mode 100755 clients/web/apps/plugins/public/artifacts/memory.surreal.graphs/0.1.0/memory.surreal.graphs.wasm create mode 100644 clients/web/apps/plugins/public/artifacts/memory.surreal.graphs/0.1.0/plugin-manifest.json delete mode 100644 clients/web/apps/plugins/vercel.json diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 927ed1e55..8098b9455 100755 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -12,6 +12,7 @@ This directory contains all GitHub Actions workflows for the starter-gradle proj | **Security** | `codeql-analysis.yml` | Security scanning with CodeQL | Push to main/minor, daily schedule | | **Publishing** | `publish-release.yml` | Publish release (Maven, Cargo, npm, Docker) | Tag push `v*.*.*` | | **Publishing** | `publish-snapshot.yml` | Publish snapshot versions | Manual, daily schedule | +| **Publishing** | `publish-plugins.yml` | Build/sign/deploy runtime plugins catalog | Plugin tag push, manual dispatch | | **Publishing** | `_publish.yml` | Reusable publish workflow | Called by other workflows | | **Automation** | `auto-fix-lockfile.yml` | Auto-update lockfiles | Daily schedule, manual | | **Automation** | `fix-renovate.yml` | Fix lockfiles for Renovate PRs | Comment `/fix-lock` on PR | @@ -189,6 +190,34 @@ Calls the reusable `_publish.yml` workflow with: --- +### `publish-plugins.yml` - Plugin Artifact Publishing + +**Purpose**: Builds, signs, verifies, and deploys runtime plugin artifacts and catalog metadata. + +**Triggers**: + +- Push tag matching `plugin//v` +- Manual trigger (`workflow_dispatch`) + +**What it does**: + +1. Resolves target plugin from `package.metadata.corvus.plugin_id` +2. Builds WASM artifact (`wasm32-wasip1`) +3. Generates immutable artifact paths under `artifacts///` +4. Upserts plugin entry into `catalog.json` and preserves existing entries +5. Signs artifact with cosign (key-based or keyless) +6. Verifies signature in CI +7. Builds plugins catalog site +8. Deploys to Cloudflare Pages (configurable) + +**Required secrets/vars for deployment**: + +- `CLOUDFLARE_API_TOKEN` +- `CLOUDFLARE_ACCOUNT_ID` +- `CLOUDFLARE_PAGES_PROJECT_NAME` (repository variable recommended) + +--- + ### `_publish.yml` - Reusable Publishing Workflow **Purpose**: Internal reusable workflow for publishing release/snapshot artifacts. diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml index 56c0de2b4..b35e54102 100644 --- a/.github/workflows/publish-plugins.yml +++ b/.github/workflows/publish-plugins.yml @@ -2,30 +2,58 @@ name: publish-plugins on: + push: + tags: + - "plugin/*/v*" workflow_dispatch: inputs: + plugin_id: + description: "Plugin ID (required for manual dispatch)" + required: false + default: "" plugin_version: - description: "Plugin version (semver)" - required: true - default: "0.1.0" + description: "Plugin version (required for manual dispatch; semver without 'v')" + required: false + default: "" + catalog_base_url: + description: "Catalog base URL for immutable artifact links" + required: false + default: "https://corvus.profiletailors.com" oci_repository: description: "OCI repository (example: ghcr.io/org/corvus-plugins/memory-surreal-graphs)" required: false + default: "" + deploy_cloudflare_pages: + description: "Deploy plugins catalog app to Cloudflare Pages" + required: false + type: boolean + default: true + cloudflare_project_name: + description: "Cloudflare Pages project name" + required: false + default: "" permissions: contents: read packages: write id-token: write +concurrency: + group: plugins-publish-${{ github.ref }} + cancel-in-progress: true + jobs: - build-and-publish-plugin: + build-sign-deploy: runs-on: ubuntu-latest env: - PLUGIN_ID: memory.surreal.graphs - PLUGIN_DIR: clients/agent-runtime/plugins/memory-surreal-graphs DIST_DIR: clients/agent-runtime/dist/plugins - OCI_REPOSITORY: ${{ inputs.oci_repository }} - PLUGIN_VERSION: ${{ inputs.plugin_version }} + INPUT_PLUGIN_ID: ${{ inputs.plugin_id }} + INPUT_PLUGIN_VERSION: ${{ inputs.plugin_version }} + INPUT_CATALOG_BASE_URL: ${{ inputs.catalog_base_url }} + INPUT_OCI_REPOSITORY: ${{ inputs.oci_repository }} + INPUT_DEPLOY_CF: ${{ inputs.deploy_cloudflare_pages }} + INPUT_CF_PROJECT_NAME: ${{ inputs.cloudflare_project_name }} + DEFAULT_CF_PROJECT_NAME: ${{ vars.CLOUDFLARE_PAGES_PROJECT_NAME }} steps: - name: ✈ Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -36,94 +64,330 @@ jobs: toolchain: stable targets: wasm32-wasip1 + - name: 🧮 Resolve plugin release target + id: meta + shell: python + run: | + import json + import os + import pathlib + import re + import tomllib + + event_name = os.environ["GITHUB_EVENT_NAME"] + ref_name = os.environ.get("GITHUB_REF_NAME", "") + + input_plugin_id = os.environ.get("INPUT_PLUGIN_ID", "").strip() + input_plugin_version = os.environ.get("INPUT_PLUGIN_VERSION", "").strip() + input_catalog_base_url = ( + os.environ.get("INPUT_CATALOG_BASE_URL", "https://corvus.profiletailors.com") + .strip() + .rstrip("/") + ) + input_oci_repository = os.environ.get("INPUT_OCI_REPOSITORY", "").strip() + input_deploy_cf = os.environ.get("INPUT_DEPLOY_CF", "true").strip().lower() + input_cf_project_name = os.environ.get("INPUT_CF_PROJECT_NAME", "").strip() + default_cf_project_name = os.environ.get("DEFAULT_CF_PROJECT_NAME", "").strip() + effective_cf_project_name = input_cf_project_name or default_cf_project_name + + if not input_catalog_base_url: + input_catalog_base_url = "https://corvus.profiletailors.com" + + if not input_deploy_cf: + input_deploy_cf = "true" + + if not input_catalog_base_url.startswith("https://"): + raise SystemExit("catalog_base_url must start with https://") + + tag_plugin_id = "" + tag_version = "" + tag_match = re.fullmatch(r"plugin/(.+)/v([0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9_.-]+)?)", ref_name) + if tag_match: + tag_plugin_id = tag_match.group(1) + tag_version = tag_match.group(2) + + if event_name == "push": + if not tag_match: + raise SystemExit( + "push trigger requires tag format plugin//v", + ) + plugin_id = tag_plugin_id + plugin_version = tag_version + else: + if not input_plugin_id or not input_plugin_version: + raise SystemExit( + "workflow_dispatch requires plugin_id and plugin_version inputs", + ) + plugin_id = input_plugin_id + plugin_version = input_plugin_version + + if any(token in plugin_id for token in ["/", "\\", ".."]): + raise SystemExit(f"invalid plugin_id: {plugin_id!r}") + + if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9_.-]+)?", plugin_version): + raise SystemExit( + "plugin_version must be semver without leading 'v'", + ) + + plugin_manifests = sorted( + pathlib.Path("clients/agent-runtime/plugins").glob("*/Cargo.toml"), + ) + selected_plugin_manifest = None + selected_plugin_data = None + + for manifest_path in plugin_manifests: + data = tomllib.loads(manifest_path.read_text(encoding="utf-8")) + metadata = data.get("package", {}).get("metadata", {}).get("corvus", {}) + manifest_plugin_id = metadata.get("plugin_id", "").strip() + if manifest_plugin_id == plugin_id: + selected_plugin_manifest = manifest_path + selected_plugin_data = data + break + + if selected_plugin_manifest is None or selected_plugin_data is None: + raise SystemExit( + f"plugin_id {plugin_id!r} not found. Ensure package.metadata.corvus.plugin_id is set.", + ) + + package = selected_plugin_data.get("package", {}) + plugin_dir = selected_plugin_manifest.parent + manifest_version = str(package.get("version", "")).strip() + if not manifest_version: + raise SystemExit("plugin Cargo.toml missing package.version") + if manifest_version != plugin_version: + raise SystemExit( + f"version mismatch: requested {plugin_version}, plugin Cargo.toml has {manifest_version}", + ) + + metadata = package.get("metadata", {}).get("corvus", {}) + crate_name = str(package.get("name", "")).replace("-", "_") + if not crate_name: + raise SystemExit("plugin Cargo.toml missing package.name") + + runtime_manifest = pathlib.Path("clients/agent-runtime/Cargo.toml") + runtime_data = tomllib.loads(runtime_manifest.read_text(encoding="utf-8")) + runtime_version = str(runtime_data.get("package", {}).get("version", "")).strip() + if not runtime_version: + raise SystemExit("clients/agent-runtime/Cargo.toml missing package.version") + + publisher = str(metadata.get("publisher", "corvus-official")).strip() or "corvus-official" + runtime_api = str(metadata.get("runtime_api", "corvus-plugin/v1")).strip() or "corvus-plugin/v1" + capabilities = metadata.get("capabilities", ["memory"]) + if not isinstance(capabilities, list) or not capabilities: + raise SystemExit("metadata.capabilities must be a non-empty array") + memory_entrypoint = str(metadata.get("memory_entrypoint", "memory_v1")).strip() or "memory_v1" + health_entrypoint = str(metadata.get("health_entrypoint", "health_v1")).strip() or "health_v1" + tools_entrypoints = metadata.get("tools_entrypoints", []) + if not isinstance(tools_entrypoints, list): + raise SystemExit("metadata.tools_entrypoints must be an array") + limits = { + "memory_mb": int(metadata.get("memory_mb", 256)), + "fuel": int(metadata.get("fuel", 1_000_000)), + "timeout_ms": int(metadata.get("timeout_ms", 10_000)), + "max_output_bytes": int(metadata.get("max_output_bytes", 262_144)), + } + + artifact_relative_path = ( + f"artifacts/{plugin_id}/{plugin_version}/{plugin_id}.wasm" + ) + signature_relative_path = f"{artifact_relative_path}.sig" + certificate_relative_path = f"{artifact_relative_path}.pem" + manifest_relative_path = ( + f"artifacts/{plugin_id}/{plugin_version}/plugin-manifest.json" + ) + artifact_url = f"{input_catalog_base_url}/{artifact_relative_path}" + signature_url = f"{input_catalog_base_url}/{signature_relative_path}" + certificate_url = f"{input_catalog_base_url}/{certificate_relative_path}" + manifest_url = f"{input_catalog_base_url}/{manifest_relative_path}" + + output = pathlib.Path(os.environ["GITHUB_OUTPUT"]) + with output.open("a", encoding="utf-8") as fh: + fh.write(f"plugin_id={plugin_id}\n") + fh.write(f"plugin_version={plugin_version}\n") + fh.write(f"plugin_dir={plugin_dir.as_posix()}\n") + fh.write(f"wasm_binary_name={crate_name}.wasm\n") + fh.write(f"runtime_version={runtime_version}\n") + fh.write(f"catalog_base_url={input_catalog_base_url}\n") + fh.write(f"oci_repository={input_oci_repository}\n") + fh.write(f"deploy_cloudflare_pages={input_deploy_cf}\n") + fh.write(f"cloudflare_project_name={effective_cf_project_name}\n") + fh.write(f"artifact_relative_path={artifact_relative_path}\n") + fh.write(f"signature_relative_path={signature_relative_path}\n") + fh.write(f"certificate_relative_path={certificate_relative_path}\n") + fh.write(f"manifest_relative_path={manifest_relative_path}\n") + fh.write(f"artifact_url={artifact_url}\n") + fh.write(f"signature_url={signature_url}\n") + fh.write(f"certificate_url={certificate_url}\n") + fh.write(f"manifest_url={manifest_url}\n") + + fh.write(f"publisher={publisher}\n") + fh.write(f"runtime_api={runtime_api}\n") + fh.write(f"memory_entrypoint={memory_entrypoint}\n") + fh.write(f"health_entrypoint={health_entrypoint}\n") + + fh.write("capabilities_json< "$DIST_DIR/plugin-manifest.json" < "$DIST_DIR/catalog.json" < "$DIST_DIR/revocations.json" < cosign.sha256 sha256sum --check --strict cosign.sha256 mv "${COSIGN_BINARY}" cosign chmod +x cosign - ./cosign sign-blob \ - --yes \ - --key env://COSIGN_PRIVATE_KEY \ - --output-signature "$DIST_DIR/$PLUGIN_ID.wasm.sig" \ - --output-certificate "$DIST_DIR/$PLUGIN_ID.wasm.pem" \ - "$DIST_DIR/$PLUGIN_ID.wasm" + + - name: ✍️ Sign plugin artifact (keyed or keyless) + env: + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + DIST_DIR: ${{ env.DIST_DIR }} + ARTIFACT_RELATIVE_PATH: ${{ steps.meta.outputs.artifact_relative_path }} + run: | + set -euo pipefail + + artifact_path="$DIST_DIR/$ARTIFACT_RELATIVE_PATH" + signature_path="${artifact_path}.sig" + certificate_path="${artifact_path}.pem" + + if [ -n "${COSIGN_PRIVATE_KEY:-}" ]; then + ./cosign sign-blob \ + --yes \ + --key env://COSIGN_PRIVATE_KEY \ + --output-signature "$signature_path" \ + --output-certificate "$certificate_path" \ + "$artifact_path" + else + ./cosign sign-blob \ + --yes \ + --output-signature "$signature_path" \ + --output-certificate "$certificate_path" \ + "$artifact_path" + fi + + - name: ✅ Verify plugin signature + env: + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + DIST_DIR: ${{ env.DIST_DIR }} + ARTIFACT_RELATIVE_PATH: ${{ steps.meta.outputs.artifact_relative_path }} + run: | + set -euo pipefail + + artifact_path="$DIST_DIR/$ARTIFACT_RELATIVE_PATH" + signature_path="${artifact_path}.sig" + certificate_path="${artifact_path}.pem" + + if [ -n "${COSIGN_PRIVATE_KEY:-}" ]; then + ./cosign public-key --key env://COSIGN_PRIVATE_KEY > cosign.pub + ./cosign verify-blob \ + --key cosign.pub \ + --signature "$signature_path" \ + "$artifact_path" + else + ./cosign verify-blob \ + --certificate "$certificate_path" \ + --signature "$signature_path" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ + --certificate-identity-regexp "https://github.com/${GITHUB_REPOSITORY}/.github/workflows/publish-plugins.yml@.*" \ + "$artifact_path" + fi - name: 🚀 Optional publish plugin artifact to OCI - if: ${{ env.OCI_REPOSITORY != '' }} + if: ${{ steps.meta.outputs.oci_repository != '' }} env: + OCI_REPOSITORY: ${{ steps.meta.outputs.oci_repository }} OCI_USERNAME: ${{ secrets.OCI_USERNAME }} OCI_PASSWORD: ${{ secrets.OCI_PASSWORD }} + PLUGIN_VERSION: ${{ steps.meta.outputs.plugin_version }} + DIST_DIR: ${{ env.DIST_DIR }} + ARTIFACT_RELATIVE_PATH: ${{ steps.meta.outputs.artifact_relative_path }} + MANIFEST_RELATIVE_PATH: ${{ steps.meta.outputs.manifest_relative_path }} run: | set -euo pipefail ORAS_VERSION="1.2.3" @@ -169,6 +488,7 @@ jobs: echo "Could not find checksum for ${ORAS_TARBALL}" exit 1 fi + printf '%s\n' "${oras_checksum_line}" > oras.sha256 sha256sum --check --strict oras.sha256 @@ -181,20 +501,124 @@ jobs: fi ./oras push "$OCI_REPOSITORY:$PLUGIN_VERSION" \ - "$DIST_DIR/$PLUGIN_ID.wasm:application/wasm" \ - "$DIST_DIR/plugin-manifest.json:application/json" \ + "$DIST_DIR/$ARTIFACT_RELATIVE_PATH:application/wasm" \ + "$DIST_DIR/${ARTIFACT_RELATIVE_PATH}.sig:application/octet-stream" \ + "$DIST_DIR/${ARTIFACT_RELATIVE_PATH}.pem:application/x-pem-file" \ + "$DIST_DIR/$MANIFEST_RELATIVE_PATH:application/json" \ "$DIST_DIR/catalog.json:application/json" \ "$DIST_DIR/revocations.json:application/json" + - name: 📦 Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 + with: + version: 10 + + - name: 📦 Setup Node.js + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: 24 + cache: "pnpm" + cache-dependency-path: clients/web/pnpm-lock.yaml + + - name: 🧩 Stage generated catalog assets + env: + DIST_DIR: ${{ env.DIST_DIR }} + ARTIFACT_RELATIVE_PATH: ${{ steps.meta.outputs.artifact_relative_path }} + SIGNATURE_RELATIVE_PATH: ${{ steps.meta.outputs.signature_relative_path }} + CERTIFICATE_RELATIVE_PATH: ${{ steps.meta.outputs.certificate_relative_path }} + MANIFEST_RELATIVE_PATH: ${{ steps.meta.outputs.manifest_relative_path }} + run: | + set -euo pipefail + + catalog_public="clients/web/apps/plugins/public" + + mkdir -p "$catalog_public/$(dirname "$ARTIFACT_RELATIVE_PATH")" + cp "$DIST_DIR/$ARTIFACT_RELATIVE_PATH" "$catalog_public/$ARTIFACT_RELATIVE_PATH" + cp "$DIST_DIR/$SIGNATURE_RELATIVE_PATH" "$catalog_public/$SIGNATURE_RELATIVE_PATH" + cp "$DIST_DIR/$CERTIFICATE_RELATIVE_PATH" "$catalog_public/$CERTIFICATE_RELATIVE_PATH" + cp "$DIST_DIR/$MANIFEST_RELATIVE_PATH" "$catalog_public/$MANIFEST_RELATIVE_PATH" + cp "$DIST_DIR/catalog.json" "$catalog_public/catalog.json" + cp "$DIST_DIR/revocations.json" "$catalog_public/revocations.json" + + - name: 📥 Install web dependencies + working-directory: clients/web + run: pnpm install --frozen-lockfile --filter @corvus/plugins-catalog... + + - name: 🏗️ Build plugins catalog site + working-directory: clients/web + env: + PLUGINS_URL: ${{ steps.meta.outputs.catalog_base_url }} + run: pnpm run build:plugins + + - name: 🚀 Optional deploy plugins catalog to Cloudflare Pages + if: ${{ steps.meta.outputs.deploy_cloudflare_pages == 'true' }} + working-directory: clients/web/apps/plugins + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_PROJECT_NAME: ${{ steps.meta.outputs.cloudflare_project_name }} + PLUGIN_ID: ${{ steps.meta.outputs.plugin_id }} + PLUGIN_VERSION: ${{ steps.meta.outputs.plugin_version }} + run: | + set -euo pipefail + + if [ -z "${CLOUDFLARE_API_TOKEN:-}" ] || [ -z "${CLOUDFLARE_ACCOUNT_ID:-}" ]; then + echo "Missing required Cloudflare secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID" + exit 1 + fi + + if [ -z "${CLOUDFLARE_PROJECT_NAME:-}" ]; then + echo "Missing cloudflare_project_name input" + exit 1 + fi + + pnpm dlx wrangler@4.44.0 pages deploy ./dist \ + --project-name "$CLOUDFLARE_PROJECT_NAME" \ + --branch main \ + --commit-hash "$GITHUB_SHA" \ + --commit-message "publish plugin ${PLUGIN_ID} ${PLUGIN_VERSION}" + - name: ⬆ Upload plugin bundle artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: - name: plugin-bundle-${{ env.PLUGIN_ID }}-${{ env.PLUGIN_VERSION }} + name: plugin-bundle-${{ steps.meta.outputs.plugin_id }}-${{ steps.meta.outputs.plugin_version }} path: | - clients/agent-runtime/dist/plugins/${{ env.PLUGIN_ID }}.wasm + clients/agent-runtime/dist/plugins/${{ steps.meta.outputs.artifact_relative_path }} + clients/agent-runtime/dist/plugins/${{ steps.meta.outputs.signature_relative_path }} + clients/agent-runtime/dist/plugins/${{ steps.meta.outputs.certificate_relative_path }} + clients/agent-runtime/dist/plugins/${{ steps.meta.outputs.manifest_relative_path }} clients/agent-runtime/dist/plugins/plugin-manifest.json clients/agent-runtime/dist/plugins/catalog.json clients/agent-runtime/dist/plugins/revocations.json - clients/agent-runtime/dist/plugins/${{ env.PLUGIN_ID }}.wasm.sig - clients/agent-runtime/dist/plugins/${{ env.PLUGIN_ID }}.wasm.pem - if-no-files-found: ignore + if-no-files-found: error + + - name: ⬆ Upload plugins catalog site artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: plugins-catalog-site-${{ steps.meta.outputs.plugin_id }}-${{ steps.meta.outputs.plugin_version }} + path: clients/web/apps/plugins/dist + if-no-files-found: error + + - name: 📝 Publish summary + env: + PLUGIN_ID: ${{ steps.meta.outputs.plugin_id }} + PLUGIN_VERSION: ${{ steps.meta.outputs.plugin_version }} + RUNTIME_VERSION: ${{ steps.meta.outputs.runtime_version }} + ARTIFACT_URL: ${{ steps.meta.outputs.artifact_url }} + MANIFEST_URL: ${{ steps.meta.outputs.manifest_url }} + CATALOG_URL: ${{ steps.meta.outputs.catalog_base_url }}/catalog.json + REVOCATIONS_URL: ${{ steps.meta.outputs.catalog_base_url }}/revocations.json + ARTIFACT_DIGEST: ${{ steps.bundle.outputs.artifact_digest }} + run: | + { + echo "## Plugin publish summary" + echo + echo "- Plugin ID: \`$PLUGIN_ID\`" + echo "- Plugin version: \`$PLUGIN_VERSION\`" + echo "- Min agent version: \`$RUNTIME_VERSION\`" + echo "- Artifact URL: \`$ARTIFACT_URL\`" + echo "- Manifest URL: \`$MANIFEST_URL\`" + echo "- Catalog URL: \`$CATALOG_URL\`" + echo "- Revocations URL: \`$REVOCATIONS_URL\`" + echo "- Artifact digest: \`$ARTIFACT_DIGEST\`" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.lycheeignore b/.lycheeignore index c75ca5d84..658da9b83 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -9,6 +9,9 @@ # Mend.io returns 403 Forbidden to bots ^https://www\.mend\.io/.* +# GNU Make website is intermittently timing out from CI/hook network +^https://www\.gnu\.org/software/make/?$ + # Checkstyle header file contains regex patterns that look like URLs gradle/configs/checkstyle/checkstyle-header-file\.txt diff --git a/clients/agent-runtime/plugins/memory-surreal-graphs/Cargo.toml b/clients/agent-runtime/plugins/memory-surreal-graphs/Cargo.toml index ac9ebf649..c228712e5 100644 --- a/clients/agent-runtime/plugins/memory-surreal-graphs/Cargo.toml +++ b/clients/agent-runtime/plugins/memory-surreal-graphs/Cargo.toml @@ -5,6 +5,19 @@ edition = "2021" license = "Apache-2.0" description = "Pilot WASM plugin artifact for Corvus surreal graph memory backend" +[package.metadata.corvus] +plugin_id = "memory.surreal.graphs" +publisher = "corvus-official" +runtime_api = "corvus-plugin/v1" +capabilities = ["memory"] +memory_entrypoint = "memory_v1" +health_entrypoint = "health_v1" +tools_entrypoints = [] +memory_mb = 256 +fuel = 1000000 +timeout_ms = 10000 +max_output_bytes = 262144 + [lib] crate-type = ["cdylib"] diff --git a/clients/agent-runtime/src/onboard/wizard.rs b/clients/agent-runtime/src/onboard/wizard.rs index 37138658e..41a62707d 100755 --- a/clients/agent-runtime/src/onboard/wizard.rs +++ b/clients/agent-runtime/src/onboard/wizard.rs @@ -2769,10 +2769,13 @@ fn setup_memory() -> Result { style("ℹ").cyan() ); let mut bootstrap = plugin_bootstrap_config(backend)?; - install_and_handle_surreal_graphs(&mut bootstrap, false)?; + install_and_handle_surreal_graphs(&mut bootstrap, true)?; + if bootstrap.memory.backend != backend { + config = bootstrap.memory; + } } - if requires_surreal_memory_setup(backend) { + if requires_surreal_memory_setup(&config.backend) { setup_surreal_memory_options(&mut config)?; } diff --git a/clients/web/apps/docs/src/content/docs/en/404.mdx b/clients/web/apps/docs/src/content/docs/en/404.mdx index 14c14978f..d9b08e8b7 100644 --- a/clients/web/apps/docs/src/content/docs/en/404.mdx +++ b/clients/web/apps/docs/src/content/docs/en/404.mdx @@ -1,19 +1,19 @@ --- -title: '404' +title: "404" template: splash editUrl: false hero: -title: '404' -tagline: Page not found. The content may have moved during Corvus customization. -actions: -- text: Back to Home -link: / -icon: right-arrow -variant: primary -- text: View Documentation -link: ../guides/getting-started/ -icon: open-book -variant: minimal + title: "404" + tagline: Page not found. The content may have moved during Corvus customization. + actions: + - text: Back to Home + link: / + icon: right-arrow + variant: primary + - text: View Documentation + link: ../guides/getting-started/ + icon: open-book + variant: minimal --- ## Looking for something? diff --git a/clients/web/apps/docs/src/content/docs/en/guides/plugins.md b/clients/web/apps/docs/src/content/docs/en/guides/plugins.md index 9056cec0a..b80cc4862 100644 --- a/clients/web/apps/docs/src/content/docs/en/guides/plugins.md +++ b/clients/web/apps/docs/src/content/docs/en/guides/plugins.md @@ -114,22 +114,45 @@ Workflow file: Current workflow behavior: -1. Build WASM plugin artifact. -2. Assemble bundle metadata: - - `plugin-manifest.json` - - `catalog.json` - - `revocations.json` -3. Optionally sign with cosign key (if secret is present). -4. Optionally push to OCI (if `oci_repository` input is provided). -5. Upload bundle artifacts. +1. Trigger automatically on plugin release tags: `plugin//v`. +2. Resolve plugin directory dynamically from `package.metadata.corvus.plugin_id` in each + plugin `Cargo.toml`. +3. Build WASM plugin artifact for `wasm32-wasip1`. +4. Assemble immutable bundle metadata and artifacts: + - `artifacts///.wasm` + - `artifacts///plugin-manifest.json` + - root `catalog.json` (upsert plugin entry, keep others) + - root `revocations.json` (preserve list, refresh `updated_at`) +5. Sign artifact with cosign (key-based when `COSIGN_PRIVATE_KEY` is set, otherwise keyless + OIDC). +6. Verify signature in CI. +7. Optionally push artifact bundle to OCI (`oci_repository`). +8. Build plugins catalog app and deploy to Cloudflare Pages (enabled by default for release tags). +9. Upload build + bundle artifacts as workflow artifacts for traceability. :::important -This workflow is currently configured for `memory.surreal.graphs` in job env defaults. For a new -plugin, either: +To onboard a new plugin into automated releases: -1. Adapt env values for the new plugin, or -2. Generalize workflow inputs/matrix so plugin ID/folder/artifact name are parameters. - ::: +1. Create it under `clients/agent-runtime/plugins//`. +2. Add `package.metadata.corvus.plugin_id` to its `Cargo.toml`. +3. Set plugin limits/capabilities in `package.metadata.corvus`. +4. Create a release tag: `plugin//v`. + +No workflow code changes are required for new plugins when metadata is present. +::: + +Release example: + +```bash +git tag plugin/memory.surreal.graphs/v0.1.0 +git push origin plugin/memory.surreal.graphs/v0.1.0 +``` + +Cloudflare deployment configuration expected by the workflow: + +- Secret: `CLOUDFLARE_API_TOKEN` +- Secret: `CLOUDFLARE_ACCOUNT_ID` +- Repository variable: `CLOUDFLARE_PAGES_PROJECT_NAME` ## 6. Operator Commands (Runtime) diff --git a/clients/web/apps/docs/src/content/docs/en/index.mdx b/clients/web/apps/docs/src/content/docs/en/index.mdx index c00bf7687..10cd92ec4 100644 --- a/clients/web/apps/docs/src/content/docs/en/index.mdx +++ b/clients/web/apps/docs/src/content/docs/en/index.mdx @@ -3,15 +3,15 @@ title: Corvus description: Reactive agent platform for always-on orchestration on the JVM. template: splash hero: -tagline: Build long-running, proactive agents with Kotlin, Spring Boot, Neo4j, Rust sidecars, and a transparent control panel. -actions: -- text: Get Started -link: guides/getting-started/ -icon: right-arrow -- text: View on GitHub -link: https://github.com/dallay/corvus -icon: external -variant: minimal + tagline: Build long-running, proactive agents with Kotlin, Spring Boot, Neo4j, Rust sidecars, and a transparent control panel. + actions: + - text: Get Started + link: guides/getting-started/ + icon: right-arrow + - text: View on GitHub + link: https://github.com/dallay/corvus + icon: external + variant: minimal --- diff --git a/clients/web/apps/docs/src/content/docs/es/404.mdx b/clients/web/apps/docs/src/content/docs/es/404.mdx index 606fb3f22..60a72520b 100644 --- a/clients/web/apps/docs/src/content/docs/es/404.mdx +++ b/clients/web/apps/docs/src/content/docs/es/404.mdx @@ -1,19 +1,19 @@ --- -title: '404' +title: "404" template: splash editUrl: false hero: -title: '404' -tagline: Página no encontrada. El contenido pudo moverse durante la personalización de Corvus. -actions: -- text: Volver al Inicio -link: / -icon: right-arrow -variant: primary -- text: Ver Documentación -link: ../guides/getting-started/ -icon: open-book -variant: minimal + title: "404" + tagline: Página no encontrada. El contenido pudo moverse durante la personalización de Corvus. + actions: + - text: Volver al Inicio + link: / + icon: right-arrow + variant: primary + - text: Ver Documentación + link: ../guides/getting-started/ + icon: open-book + variant: minimal --- ## ¿Buscas algo? diff --git a/clients/web/apps/docs/src/content/docs/es/guides/plugins.md b/clients/web/apps/docs/src/content/docs/es/guides/plugins.md index 016b0245e..aebb5d032 100644 --- a/clients/web/apps/docs/src/content/docs/es/guides/plugins.md +++ b/clients/web/apps/docs/src/content/docs/es/guides/plugins.md @@ -115,23 +115,46 @@ Archivo de workflow: Comportamiento actual del workflow: -1. Build del artefacto WASM del plugin. -2. Ensamblar metadatos del bundle: - - `plugin-manifest.json` - - `catalog.json` - - `revocations.json` -3. Opcionalmente firmar con clave cosign (si el secret está presente). -4. Opcionalmente push a OCI (si el input `oci_repository` es proporcionado). -5. Upload de artefactos del bundle. +1. Se dispara automáticamente con tags de release: `plugin//v`. +2. Resuelve dinámicamente la carpeta del plugin usando `package.metadata.corvus.plugin_id` en + cada `Cargo.toml`. +3. Hace build del artefacto WASM para `wasm32-wasip1`. +4. Ensambla bundle inmutable con artefactos/metadatos: + - `artifacts///.wasm` + - `artifacts///plugin-manifest.json` + - `catalog.json` raíz (upsert del plugin, preservando los demás) + - `revocations.json` raíz (preserva la lista, actualiza `updated_at`) +5. Firma el artefacto con cosign (con clave cuando existe `COSIGN_PRIVATE_KEY`, si no keyless + OIDC). +6. Verifica la firma en CI. +7. Push opcional del bundle a OCI (`oci_repository`). +8. Hace build del app de catálogo y deploy a Cloudflare Pages (habilitado por defecto para tags de + release). +9. Sube artefactos del build y del bundle para trazabilidad. :::important -Este workflow está actualmente configurado para `memory.surreal.graphs` en los defaults de env del -job. Para un nuevo plugin, ya sea: +Para integrar un nuevo plugin al release automático: -1. Adaptar los valores de env para el nuevo plugin, o -2. Generalizar los inputs/matrix del workflow para que plugin ID/folder/nombre de artefacto sean - parámetros. - ::: +1. Créalo en `clients/agent-runtime/plugins//`. +2. Agrega `package.metadata.corvus.plugin_id` en su `Cargo.toml`. +3. Define límites/capabilities en `package.metadata.corvus`. +4. Crea un tag de release: `plugin//v`. + +Con metadata correcta, no hace falta cambiar el workflow para plugins nuevos. +::: + +Ejemplo de release: + +```bash +git tag plugin/memory.surreal.graphs/v0.1.0 +git push origin plugin/memory.surreal.graphs/v0.1.0 +``` + +Configuración de Cloudflare esperada por el workflow: + +- Secret: `CLOUDFLARE_API_TOKEN` +- Secret: `CLOUDFLARE_ACCOUNT_ID` +- Variable de repositorio: `CLOUDFLARE_PAGES_PROJECT_NAME` ## 6. Comandos de Operador (Runtime) diff --git a/clients/web/apps/docs/src/content/docs/es/index.mdx b/clients/web/apps/docs/src/content/docs/es/index.mdx index 7630baeef..c012b0e43 100644 --- a/clients/web/apps/docs/src/content/docs/es/index.mdx +++ b/clients/web/apps/docs/src/content/docs/es/index.mdx @@ -3,15 +3,15 @@ title: Corvus description: Plataforma reactiva de agentes siempre activos sobre JVM. template: splash hero: -tagline: Construye agentes proactivos y de larga duración con Kotlin, Spring Boot, Neo4j, sidecars en Rust y un panel transparente. -actions: -- text: Primeros Pasos -link: guides/getting-started/ -icon: right-arrow -- text: Ver en GitHub -link: https://github.com/dallay/corvus -icon: external -variant: minimal + tagline: Construye agentes proactivos y de larga duración con Kotlin, Spring Boot, Neo4j, sidecars en Rust y un panel transparente. + actions: + - text: Primeros Pasos + link: guides/getting-started/ + icon: right-arrow + - text: Ver en GitHub + link: https://github.com/dallay/corvus + icon: external + variant: minimal --- diff --git a/clients/web/apps/docs/src/pages/404.astro b/clients/web/apps/docs/src/pages/404.astro deleted file mode 100644 index 416dcf8e4..000000000 --- a/clients/web/apps/docs/src/pages/404.astro +++ /dev/null @@ -1,273 +0,0 @@ ---- -// biome-ignore lint/correctness/noUnusedImports: Used in Astro template below. -import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro' -// biome-ignore lint/correctness/noUnusedImports: Used in Astro template below. -import { Icon } from '@astrojs/starlight/components' - -const baseUrl = import.meta.env.BASE_URL -// biome-ignore lint/correctness/noUnusedVariables: Used in Astro template below. -const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/` - -// Detect locale from pathname (e.g., /corvus/es/...) -const pathname = Astro.url.pathname -const isEs = pathname.includes('/es/') -const locale = isEs ? 'es' : 'en' - -const t = { - en: { - title: '404', - tagline: "The page you're looking for has disappeared from the repository.", - home: 'Back to Home', - repo: 'View Repository', - gettingStarted: 'Getting Started', - structure: 'Project Structure', - }, - es: { - title: '404', - tagline: 'La página que buscas ha desaparecido en el repositorio.', - home: 'Volver al inicio', - repo: 'Ver Repositorio', - gettingStarted: 'Primeros pasos', - structure: 'Estructura', - }, -} - -// biome-ignore lint/correctness/noUnusedVariables: Used in Astro template below. -const labels = t[locale] ---- - - - - - - diff --git a/clients/web/apps/plugins/README.md b/clients/web/apps/plugins/README.md index af2bd0dac..6cc8c1568 100644 --- a/clients/web/apps/plugins/README.md +++ b/clients/web/apps/plugins/README.md @@ -4,6 +4,7 @@ Dedicated site for publishing official runtime plugin metadata: - `catalog.json` - `revocations.json` +- versioned immutable plugin artifacts under `artifacts///` ## Purpose @@ -38,3 +39,32 @@ PLUGINS_URL=https://staging-catalog.profiletailors.com pnpm build:plugins - Publish `catalog.json` and `revocations.json` at the site root. - Keep `revocations.json` uncached (`no-store`) for immediate revocation propagation. +- Publish WASM artifacts at immutable, versioned paths. + - Example: `/artifacts/memory.surreal.graphs/0.1.0/memory.surreal.graphs.wasm` + - Use long-lived immutable cache headers for artifact paths. + +## Automated Plugin Release (Cloudflare Pages) + +Plugin publishing is automated by `.github/workflows/publish-plugins.yml`. + +- Automatic trigger (recommended): push a tag using format + `plugin//v`. +- Manual trigger: workflow dispatch with `plugin_id` + `plugin_version`. + +Required plugin metadata in each plugin `Cargo.toml`: + +```toml +[package.metadata.corvus] +plugin_id = "memory.surreal.graphs" +``` + +Required repository secrets/variables for Cloudflare deployment: + +- Secret: `CLOUDFLARE_API_TOKEN` +- Secret: `CLOUDFLARE_ACCOUNT_ID` +- Variable (recommended): `CLOUDFLARE_PAGES_PROJECT_NAME` + +Optional signing/publishing secrets: + +- `COSIGN_PRIVATE_KEY` and `COSIGN_PASSWORD` (key-based signing) +- `OCI_USERNAME` and `OCI_PASSWORD` (when publishing to OCI) diff --git a/clients/web/apps/plugins/public/_headers b/clients/web/apps/plugins/public/_headers index a9c1aab62..b7ded0f46 100644 --- a/clients/web/apps/plugins/public/_headers +++ b/clients/web/apps/plugins/public/_headers @@ -7,3 +7,27 @@ Content-Type: application/json; charset=utf-8 Cache-Control: no-store, max-age=0 X-Content-Type-Options: nosniff + +/artifacts/*/*/*.wasm + Content-Type: application/wasm + Cache-Control: public, max-age=31536000, immutable + X-Content-Type-Options: nosniff + +/artifacts/*/*/*.json + Content-Type: application/json; charset=utf-8 + Cache-Control: public, max-age=31536000, immutable + X-Content-Type-Options: nosniff + +/artifacts/*/*/*.sig + Content-Type: application/octet-stream + Cache-Control: public, max-age=31536000, immutable + X-Content-Type-Options: nosniff + +/artifacts/*/*/*.pem + Content-Type: application/x-pem-file + Cache-Control: public, max-age=31536000, immutable + X-Content-Type-Options: nosniff + +/artifacts/*/*/* + Cache-Control: public, max-age=31536000, immutable + X-Content-Type-Options: nosniff diff --git a/clients/web/apps/plugins/public/artifacts/memory.surreal.graphs/0.1.0/memory.surreal.graphs.wasm b/clients/web/apps/plugins/public/artifacts/memory.surreal.graphs/0.1.0/memory.surreal.graphs.wasm new file mode 100755 index 0000000000000000000000000000000000000000..02b7bdab6c27cbba96ea3f4fac30f6bf62e0fb2c GIT binary patch literal 99 zcmXZMyA8rn5Jkar_kA#|gth}}aVI7qdRevocations /revocations.json

+

+ Immutable artifact + /artifacts/memory.surreal.graphs/0.1.0/memory.surreal.graphs.wasm +

+

+ Immutable manifest + /artifacts/memory.surreal.graphs/0.1.0/plugin-manifest.json +

Runtime defaults point to these URLs and treat them as official metadata sources.

@@ -36,6 +44,12 @@ import CatalogLayout from "../layouts/CatalogLayout.astro";

curl -fsSL https://corvus.profiletailors.com/revocations.json

+

+ curl -fsSL https://corvus.profiletailors.com/artifacts/memory.surreal.graphs/0.1.0/memory.surreal.graphs.wasm -o memory.surreal.graphs.wasm +

+

+ curl -fsSL https://corvus.profiletailors.com/artifacts/memory.surreal.graphs/0.1.0/plugin-manifest.json +