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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "terraphim-skills-intro",
"source": "./",
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"version": "0.2.5",
"version": "0.2.6",
"category": "development",
"homepage": "https://terraphim-skills.md/"
}
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "terraphim-skills-intro",
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"version": "0.2.5",
"version": "0.2.6",
"author": {
"name": "Terraphim AI",
"email": "engineering@terraphim.ai"
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "terraphim-skills-intro",
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"version": "0.2.5",
"version": "0.2.6",
"author": {
"name": "Terraphim AI",
"email": "engineering@terraphim.ai"
Expand Down
41 changes: 32 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ jobs:

publish:
needs: validate
concurrency:
group: skills-publish-${{ github.ref }}
cancel-in-progress: false
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -99,12 +102,32 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
dist/autoclaw/*.zip \
dist/autoclaw/SHA256SUMS \
assets/v-model-overview.svg \
assets/v-model-overview.png \
--repo "$GITHUB_REPOSITORY" \
--title "Terraphim Skills Introduction $GITHUB_REF_NAME" \
--generate-notes \
--verify-tag
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --json tagName; then
# A retry never uploads or replaces existing assets. The next step
# verifies the complete immutable release against this tagged source.
echo "Release already exists; verifying its published assets."
else
gh release create "$GITHUB_REF_NAME" \
dist/autoclaw/*.zip \
dist/autoclaw/SHA256SUMS \
assets/v-model-overview.svg \
assets/v-model-overview.png \
--repo "$GITHUB_REPOSITORY" \
--title "Terraphim Skills Introduction $GITHUB_REF_NAME" \
--generate-notes \
--verify-tag
fi

- name: Read back every public release asset
run: |
python3 scripts/verify_published_release.py \
--tag "$GITHUB_REF_NAME" \
--receipt "$RUNNER_TEMP/published-release.json"

- name: Retain actual public readback
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: published-release-readback
path: ${{ runner.temp }}/published-release.json
if-no-files-found: error
53 changes: 53 additions & 0 deletions .github/workflows/verify-published.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Verify published marketplace pack

on:
workflow_dispatch:
inputs:
tag:
description: Existing immutable signed release tag to verify
required: true
type: string

permissions:
contents: read

jobs:
readback:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Resolve and verify the original signed release
env:
RELEASE_TAG: ${{ inputs.tag }}
shell: bash
run: |
set -euo pipefail
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]
release_ref="refs/release-readback/$RELEASE_TAG"
git fetch --no-tags origin "refs/tags/$RELEASE_TAG:$release_ref"
git -c gpg.format=ssh \
-c gpg.ssh.allowedSignersFile="$GITHUB_WORKSPACE/.github/trusted-release-signers" \
verify-tag "$release_ref"
mkdir "$RUNNER_TEMP/release-source"
git archive "$release_ref" | tar -x -C "$RUNNER_TEMP/release-source"
- name: Read back public release bytes
env:
RELEASE_TAG: ${{ inputs.tag }}
run: |
python3 scripts/verify_published_release.py \
--root "$RUNNER_TEMP/release-source" --tag "$RELEASE_TAG" \
--receipt "$RUNNER_TEMP/published-release.json"
- name: Retain actual public readback
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: published-release-readback
path: ${{ runner.temp }}/published-release.json
if-no-files-found: error
2 changes: 1 addition & 1 deletion .zcode-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "terraphim-skills-intro",
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"version": "0.2.5",
"version": "0.2.6",
"author": {
"name": "Terraphim AI",
"email": "engineering@terraphim.ai"
Expand Down
8 changes: 4 additions & 4 deletions dist/autoclaw/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
218e953aa259b7bffd866e95ccfebe7ebe2d0a9bcdd8c8d7fe7d23689037517c terraphim-grep-0.2.5.zip
72133ba7d0e87158d1fef420ee4a194f6a32113ded1ad9c077e01a7ef1bc36d2 terraphim-agent-learn-0.2.5.zip
bd9c8eee9f3b2d177424dd70cebef891d6e65f2946f6766daaea728c236ee796 terraphim-agent-memory-0.2.5.zip
fa15eff9b196591c99b1cd5cd6493162a6658dc8d1a44a7f014177c546c7075c terraphim-editor-0.2.5.zip
218e953aa259b7bffd866e95ccfebe7ebe2d0a9bcdd8c8d7fe7d23689037517c terraphim-grep-0.2.6.zip
72133ba7d0e87158d1fef420ee4a194f6a32113ded1ad9c077e01a7ef1bc36d2 terraphim-agent-learn-0.2.6.zip
bd9c8eee9f3b2d177424dd70cebef891d6e65f2946f6766daaea728c236ee796 terraphim-agent-memory-0.2.6.zip
fa15eff9b196591c99b1cd5cd6493162a6658dc8d1a44a7f014177c546c7075c terraphim-editor-0.2.6.zip
2 changes: 1 addition & 1 deletion distribution/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"plugin": {
"name": "terraphim-skills-intro",
"display_name": "Terraphim Skills Introduction",
"version": "0.2.5",
"version": "0.2.6",
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"description_zh_cn": "在编辑器中创建备选措辞、搜索共享同义词图、记录经验并检索记忆。",
"author_name": "Terraphim AI",
Expand Down
26 changes: 26 additions & 0 deletions docs/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,29 @@ phrase “signed, checksummed release archives” to “checksummed release arch
Their provenance records the exact old/new text and source version/commit, and
the verifier permits only that reviewed correction. Original signed source files
and existing release assets remain unchanged.

## CI delivery and external catalog gates

An SSH-signed version tag runs original TSM signature verification, tests and
reproducible archive checks before creating an immutable GitHub release. CI now
reads every public release asset back and compares it with the reviewed source;
`published-release-readback` retains the actual download hashes. The read-only
**Verify published marketplace pack** workflow can check an existing signed tag
without recreating a release. A failed download or byte mismatch fails the job.

Kimi and ZCode publisher manifests pin this source release. Claude, Cursor,
Hermes and skills.sh use the checked repository manifests or skill trees;
AutoClaw uses its per-skill ZIPs. These delivery artifacts are generated and
verified together, while native installation remains a separate host check.

Curated Kilo, Grok and ZCode catalogs require their maintainers to accept the
existing submissions. Kilo's updater fix and four-skill submission remain
separate pull requests. CI must not report those pending admissions as published
listings. A successful source release does not merge another repository's PR.

The Terraphim API catalog is a separate publishing surface. Its deployed publish
endpoint requires a publisher OAuth grant for the CLI audience; the current
service has no GitHub OIDC publisher exchange. Existing catalog uploads use
`tsm push`, and service/site deployment CI does not upload new skill content.
Unattended catalog publication remains a gate until a supported scoped CI
publisher identity is available; no human token is copied into this repository.
2 changes: 1 addition & 1 deletion kimi.plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "terraphim-skills-intro",
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"version": "0.2.5",
"version": "0.2.6",
"author": {
"name": "Terraphim AI",
"email": "engineering@terraphim.ai"
Expand Down
2 changes: 1 addition & 1 deletion marketplaces/kimi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"id": "terraphim-skills-intro",
"displayName": "Terraphim Skills Introduction",
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"source": "https://github.com/terraphim/terraphim-cursor-plugin/archive/refs/tags/v0.2.5.zip"
"source": "https://github.com/terraphim/terraphim-cursor-plugin/archive/refs/tags/v0.2.6.zip"
}
]
}
4 changes: 2 additions & 2 deletions marketplaces/zcode.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
"source": "github",
"repo": "terraphim/terraphim-cursor-plugin",
"path": ".",
"ref": "v0.2.5"
"ref": "v0.2.6"
},
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"description_i18n": {
"en": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"zh-CN": "在编辑器中创建备选措辞、搜索共享同义词图、记录经验并检索记忆。"
},
"version": "0.2.5",
"version": "0.2.6",
"category": "developer-tools",
"tags": [
"terraphim",
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "terraphim-skills-intro",
"description": "Create editor alternatives, search a shared synonym graph, capture learning, and retrieve memory with Terraphim.",
"version": "0.2.5",
"version": "0.2.6",
"author": {
"name": "Terraphim AI",
"email": "engineering@terraphim.ai"
Expand Down
96 changes: 96 additions & 0 deletions scripts/verify_published_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Read back every immutable public release asset and compare reviewed bytes."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import re
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import quote
from urllib.request import Request, urlopen

REPOSITORY = 'terraphim/terraphim-cursor-plugin'
MAX_ASSET_BYTES = 32 * 1024 * 1024

class ReleaseError(RuntimeError):
"""Published bytes do not satisfy the reviewed release contract."""

def _read(url: str, timeout: float, limit: int) -> bytes:
try:
with urlopen(Request(url, headers={'User-Agent': 'terraphim-release-readback/1'}), timeout=timeout) as response:
if response.status != 200:
raise ReleaseError(f'HTTP {response.status} for {url}')
data = response.read(limit + 1)
if len(data) > limit:
raise ReleaseError(f'response exceeds byte limit: {url}')
return data
except HTTPError as error:
error.close()
raise ReleaseError(f'HTTP {error.code} for {url}') from error
except (URLError, TimeoutError, OSError) as error:
raise ReleaseError(f'cannot read {url}: {error}') from error

def verify_release(root: Path, tag: str, *, api_url: str | None = None,
asset_base: str | None = None, timeout: float = 15) -> dict:
"""Compare actual metadata and downloads with an already verified source tag."""
if not re.fullmatch(r'v[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?', tag):
raise ReleaseError('invalid release tag')
if timeout <= 0:
raise ReleaseError('timeout must be positive')
try:
version = json.loads((root/'distribution/metadata.json').read_text())['plugin']['version']
except (OSError, ValueError, KeyError, TypeError) as error:
raise ReleaseError('invalid source distribution metadata') from error
if tag != f'v{version}':
raise ReleaseError('source version differs from release tag')
paths = [*sorted((root/'dist/autoclaw').glob('*.zip')), root/'dist/autoclaw/SHA256SUMS',
root/'assets/v-model-overview.svg', root/'assets/v-model-overview.png']
if len(paths) < 4 or any(not path.is_file() for path in paths):
raise ReleaseError('source release assets missing')
expected = {path.name:path for path in paths}
api_url = api_url or f'https://api.github.com/repos/{REPOSITORY}/releases/tags/{tag}'
asset_base = asset_base or f'https://github.com/{REPOSITORY}/releases/download/{tag}/'
try:
release = json.loads(_read(api_url, timeout, 1024 * 1024))
except (ValueError, UnicodeError) as error:
raise ReleaseError('release metadata is not JSON') from error
if not isinstance(release, dict) or release.get('tag_name') != tag or release.get('draft') is not False:
raise ReleaseError('release identity differs or is still a draft')
assets = release.get('assets')
if not isinstance(assets, list) or any(not isinstance(item, dict) for item in assets):
raise ReleaseError('release assets are invalid')
names = [item.get('name') for item in assets]
if any(not isinstance(name, str) for name in names) or len(names) != len(set(names)) or set(names) != set(expected):
raise ReleaseError('release asset set differs from reviewed source')
results = {}
for item in assets:
name = item['name'];url = asset_base + quote(name, safe='');source = expected[name].read_bytes()
if item.get('browser_download_url') != url or item.get('state') != 'uploaded' or item.get('size') != len(source):
raise ReleaseError(f'invalid publication metadata for {name}')
if len(source) > MAX_ASSET_BYTES:
raise ReleaseError(f'source asset exceeds byte limit: {name}')
downloaded = _read(url, timeout, MAX_ASSET_BYTES)
if downloaded != source:
raise ReleaseError(f'public asset differs from reviewed source: {name}')
results[name] = {'bytes': len(downloaded), 'sha256': hashlib.sha256(downloaded).hexdigest(), 'url': url}
return {'status': 'PASS', 'repository': REPOSITORY, 'tag': tag, 'assets': results,
'scope': 'Actual public downloads equal verified source assets; curated listing and native host acceptance are separate.'}

def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument('--tag', required=True)
parser.add_argument('--receipt', type=Path, required=True)
args = parser.parse_args()
try:
result = verify_release(args.root, args.tag);status = 0
except ReleaseError as error:
result = {'status': 'FAIL', 'tag': args.tag, 'error': str(error)};status = 1
args.receipt.write_text(json.dumps(result, indent=2)+'\n')
print(json.dumps(result))
return status

if __name__ == '__main__':
sys.exit(main())
6 changes: 3 additions & 3 deletions tests/test_distribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ def test_exact_public_skill_allowlist_and_tree(self) -> None:
self.assertEqual(GENERATOR.ALLOWED_SKILLS, names)

def test_print_version_matches_manifests(self) -> None:
self.assertEqual("0.2.5", self.metadata["plugin"]["version"])
self.assertEqual("0.2.6", self.metadata["plugin"]["version"])
output = io.StringIO()
with redirect_stdout(output):
self.assertEqual(0, GENERATOR.main(["--print-version"]))
self.assertEqual("0.2.5\n", output.getvalue())
self.assertEqual("0.2.6\n", output.getvalue())

def test_generated_manifests_are_current_and_deterministic(self) -> None:
expected = GENERATOR.generated_files(ROOT, self.metadata)
Expand Down Expand Up @@ -93,7 +93,7 @@ def test_zcode_marketplace_pins_release_and_category(self) -> None:
entry = marketplace["plugins"][0]
self.assertEqual("terraphim-skills-intro", entry["name"])
self.assertEqual("developer-tools", entry["category"])
self.assertEqual("v0.2.5", entry["source"]["ref"])
self.assertEqual("v0.2.6", entry["source"]["ref"])
self.assertTrue(entry["strict"])
self.assertEqual({"en", "zh-CN"}, set(entry["description_i18n"]))
manifest = json.loads((ROOT / ".zcode-plugin/plugin.json").read_text())
Expand Down
Loading
Loading