feat(apigee-skills-serving): add reference for Apigee API hub as agent skill catalog#889
Merged
OmidTahouri merged 7 commits intoJun 30, 2026
Merged
Conversation
…t skill catalog Adds a new reference implementation under references/apigee-skills-serving demonstrating how to use Apigee API hub as a versioned, signed catalog of agent skills (SKILL.md bundles) discoverable and installable by LLM agent runtimes such as OpenCode, Claude Code, and Gemini CLI. What's included: * Publisher toolchain (scripts/): pack/sign/upload/register/update-taxonomy Python scripts that turn a directory of SKILL.md + manifest.yaml into a signed .skill archive uploaded to GCS and registered with API hub. * Locked v1 manifest schema (schema/skill-manifest.schema.yaml) with Ed25519 detached-signature support over canonical YAML serialisation. * Three example skills (skills/): currency-converter, weather-lookup, apigee-policy-top10 (a non-trivial skill that documents the ten most recommended Apigee policy patterns and enumerates which are deployed in your org). * One showcase example (examples/apigee-proxy-skill): a production-shaped skill that scaffolds, validates, packages, and deploys Apigee X / hybrid proxies via an MCP server. * End-to-end documentation (docs/): architecture, publish-and-install walkthrough, policy-skill-catalog deep-dive. * Hermetic test suite (tests/): 220 unit + in-process integration tests, all HTTP and ADC mocked, no live GCP needed, runs in ~1.3s. * pipeline.sh: devrel CI entry-point that installs deps into a venv and runs the test suite. Also updates: * CODEOWNERS: adds @gsjurseth as owner of the new reference. * README.md: adds the new reference to the References section. The implementation is independent of any specific agent runtime; the SKILL.md format and skill install path (~/.config/opencode/skills/) match the OpenCode convention, which is the closest thing to an emerging de-facto standard in the OSS agent runtime space.
Collaborator
|
/gcbrun |
Pipeline Report
View details in Cloud Build (permission required) Commit version: b2c61d9 |
Two CI fixes from PR apigee#889 first run: 1. PYTHON_ISORT (Lint Codebase job): 12 Python files had import groupings that violate isort defaults (multi-line imports that fit on one line; trailing blank line after imports). Auto-fixed with `isort scripts/ tests/`. 2. License Headers job: 16 files in tests/ were missing the Apache 2.0 header. (My earlier bulk-header script targeted scripts/ and skills/ only; tests/ was overlooked.) All 16 now have the standard header. Verified locally: - isort --check-only scripts/ tests/ (clean) - pytest -q tests/ (220/220 pass)
Collaborator
|
/gcbrun |
Pipeline Report
View details in Cloud Build (permission required) Commit version: eda2aa7 |
Fixes remaining CI lint failures on PR apigee#889 across four linters: flake8 (W391, F401, E265, E741): - Removed trailing blank line from 2 __init__.py files - Removed 7 unused imports across 5 test files - Renamed ambiguous variable `l` to `line` in a comprehension - Moved shebang to line 1 in top10.py (was below license header) bandit (B311, B405, B314): - Annotated random.randint() for retry jitter with `# nosec B311` (random is not used in a cryptographic context) - Annotated xml.etree.ElementTree import and ET.fromstring() with `# nosec B405` / `# nosec B314` (XML is fetched from authenticated Apigee API, not untrusted user input) yamllint (indentation): - Re-indented block sequences in 4 manifest.yaml files from 0-indent to 2-indent per yamllint default (`keywords:\n - item` instead of `keywords:\n- item`) Verified locally with CI-matching versions: - isort 5.9.3 --check-only: clean - flake8 7.3.0: clean - bandit 1.7.0: no issues - yamllint: clean - pytest -q tests/: 220/220 pass
…=100 Megalinter v4 invokes isort with an effective line-length of 100 (matching flake8's --max-line-length=100). Four files had imports wrapped across two lines that actually fit on a single 100-char line. CI's isort wants the unwrapped form; my local v8 isort defaulted to 79 and wrapped them. Verified with isort 5.9.3 (CI's version) at --line-length=100: clean. Tests: 220/220 still pass.
…up.sh
MegaLinter's shellcheck step blocks PR#889 with three SC2015 hits in
bin/demo-cleanup.sh (lines 90, 101, 111):
cmd && ((removed++)) || ((kept++))
This is the classic "A && B || C is not if-then-else" trap, and in
this script it is also a real double-counting bug, not just a style
nit:
- ((removed++)) is post-increment: bash arithmetic evaluates the
*current* value of `removed` first, then increments. So when
`removed` is still 0, ((removed++)) returns exit status 1
(because ((0)) is falsy), even though the rm succeeded.
- The `||` branch then fires and ((kept++)) also runs, so a
single successful removal increments BOTH counters and the
cleanup summary lies about what was removed vs. what was
already absent.
Replaced all three sites with explicit `if/then/else` blocks. Added a
short comment at the first occurrence so a future reader does not
"simplify" the if/else back to the broken short form.
Verified locally:
- shellcheck v0.11.0 on all five shell scripts in the PR: PASS
- bash -n on demo-cleanup.sh: PASS
- Full megalinter v4 run via podman against the entire devrel tree
(same env vars as .github/workflows/devrel-static-checks.yml):
"Successfully linted all files, but with ignored errors" -- zero
blocking failures, all remaining issues are pre-existing
non-blocking categories already present on main.
No other production-source files touched.
Collaborator
|
/gcbrun |
Pipeline Report
View details in Cloud Build (permission required) Commit version: 5c2650d |
…ir patch
The Cloud Build pipeline (apigee-devrel-pr) runs pytest as root,
which exposed a latent bug in
test_env_enabled_readonly_dir_returns_UNDETECTABLE.
The test was trying to provoke an OSError by chmod'ing tmp_path
to 0o555 (read+exec, no write):
tmp_path.chmod(0o555)
state = detect_watcher(skills_dir=tmp_path, ...)
assert state == WatcherState.WATCHER_UNDETECTABLE
That works for an unprivileged user. It does not work for root,
which has CAP_DAC_OVERRIDE and silently ignores POSIX write bits.
On CI, mkdir then succeeded, detect_watcher returned ENABLED, and
the test failed with:
AssertionError: assert <WatcherState.WATCHER_ENABLED> ==
<WatcherState.WATCHER_UNDETECTABLE>
The contract under test is "if mkdir raises OSError, the probe
returns UNDETECTABLE so the caller can fall back to /reload-skills."
That contract does not depend on whether the filesystem actually
enforces 0o555 -- it depends on what happens when mkdir raises.
Replaced the chmod with a scoped monkeypatch on Path.mkdir that
raises PermissionError(13) for the .probe-<uuid> directory while
letting the outer skills_dir.mkdir(exist_ok=True) succeed (same
shape detect_watcher relies on). This tests the contract directly,
without depending on the runtime user.
Verified:
- Local pytest as user gsjurseth: 9/9 GREEN.
- podman run as uid=0(root) inside python:3.13-slim
(exact CI condition): 9/9 GREEN; full suite 220/220 GREEN.
…leanup.sh
bin/demo-cleanup.sh has called _remove_file since the file was first
committed (lines that clean up .staging.lock and .recent-install)
but the function itself was never defined. Operators hit four
"command not found" lines whenever they ran the script, and the
removed/kept counters reported wrong totals because every call
fell through to the missing-function path (exit status 127).
This was not caught by CI because:
- shellcheck does not flag undefined-function calls
- the pipeline pytest suite does not exercise the shell scripts
- the file/symlink targets often do not exist on the CI
container, so the cleanup loop never actually invokes the
failing branch in a way that surfaces
Added _remove_file as a sibling to _remove_dir:
- same (path, dry_run, verbose) interface
- same [[ -e || -L ]] guard so dangling symlinks are still
detected and cleaned
- uses `rm -f` rather than `rm -rf` because the cleanup
targets are normal files, not directory trees -- preserving
the original intent of the now-missing function
- separate "(file)" vs "(dir)" verbose-mode messages so the
operator can tell the two helpers apart
Verified locally with a dry-run (--dry-run --verbose): all six
cleanup targets are reported correctly, the counters add up
(2 would-remove + 4 already-absent = 6 total iterations), and
no "command not found" lines remain.
shellcheck on the modified file continues to PASS.
Collaborator
|
/gcbrun |
Pipeline Report
View details in Cloud Build (permission required) Commit version: dc1c40c |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new reference under
references/apigee-skills-servingdemonstratinghow to use Apigee API hub as a versioned, signed catalog of agent skills
(SKILL.md bundles) discoverable and installable by LLM agent runtimes such as
OpenCode, Claude Code, and Gemini CLI.
The contribution is fully self-contained: pipeline runs the hermetic 220-test
Python suite (no live GCP needed), all sources have Apache-2.0 headers,
CODEOWNERS and the root README list the new reference.
Why this fits in
references/API hub is increasingly being used as a discovery surface for things other
than traditional REST APIs (notebooks, prompts, MCP toolsets). Agent skills
are a natural fit: small bundles of LLM instructions + helper scripts,
content-addressed and signed. This reference shows the publisher and
consumer contracts end-to-end:
pack,sign(Ed25519 over canonical YAML),uploadto GCS,
registerwith API hub,update_taxonomyfor the four user-definedattributes (
skill-compatible,skill-runtime-iam,skill-signing-key-id,skill-bundle-gs-uri).in-process integration of register + fetch that exercises it end-to-end
without contacting GCP.
What's included
scripts/common/libs)schema/skill-manifest.schema.yamlskills/currency-converter/,weather-lookup/skills/apigee-policy-top10/examples/apigee-proxy-skill/bin/check-prerequisites.sh,demo-setup.sh,demo-cleanup.shdocs/tests/pipeline.shpytest -q)Verification
pipeline.shruns green locally: 220/220 tests pass in a fresh venv.shellcheck bin/*.sh pipeline.sh: zero errors/warnings, only 3 SC2015info notices on intentional counter-idioms.
python3 -m py_compile).yaml.safe_load).Notes for reviewers
Scope. This is larger than a typical
references/entry (~10k LOC,mostly Python tests). I sized it down from the upstream development repo
by dropping the consumer-side
skill-finderruntime and 6 test filesthat hard-depend on it; the publisher contract is fully validated by the
remaining 220 tests including a stateful in-process register/fetch
round-trip. Happy to split into smaller PRs if preferred.
No live cloud needed for CI. Every HTTP call, ADC lookup, and GCP
service is mocked at the
requests/google.authboundary. A reviewercan run
./pipeline.shon their laptop without an Apigee org or APIhub instance and see all 220 tests pass.
examples/apigee-proxy-skill/ships a signed manifest with signingfields stripped — the
signature,signing_key_id,zip_sha256, andgs_urifields are removed because they referenced an internal demobucket. Users regenerate them by running
scripts/sign_skill.py+scripts/upload_skill.py. A comment at the top of each manifestdocuments this.
SKILL.mdformat. This follows the OpenCode convention(
~/.config/opencode/skills/{name}/), which is the closest thing to anemerging de-facto standard in the OSS agent runtime space. The
reference is runtime-agnostic; any agent that can read frontmatter +
markdown + invoke scripts can consume these skills.
CLA. I'm a Googler and have signed the corporate CLA.
Drafting this PR
Opening as draft to give the apigee/devrel maintainers a chance to react
to the scope before the formal review starts. Per CONTRIBUTING.md's guidance
on large PRs, please flag if you'd prefer this discussed as an issue first
or split into smaller chunks.
Checklist
pipeline.shruns locally