Skip to content

feat(apigee-skills-serving): add skill-finder + skill-publisher + one-line provisioning#891

Open
gsjurseth wants to merge 7 commits into
apigee:mainfrom
gsjurseth:feat/add-skill-finder
Open

feat(apigee-skills-serving): add skill-finder + skill-publisher + one-line provisioning#891
gsjurseth wants to merge 7 commits into
apigee:mainfrom
gsjurseth:feat/add-skill-finder

Conversation

@gsjurseth

Copy link
Copy Markdown
Contributor

Summary

Extends the references/apigee-skills-serving/ reference (merged in
#889) with two shipped agent skills and a one-line onboarding
experience.

New skills:

  • skills/skill-finder/ — consumer-side discovery+install client.
    Queries the customer's API hub for signed skills, verifies their
    Ed25519 signatures against locally-installed per-deployment trust
    roots, and installs into the runtime skills directory (Gemini CLI,
    OpenCode, Antigravity).
  • skills/skill-publisher/ — author-side pipeline orchestrator.
    Wraps pack → sign → upload → register into a single skill an agent
    can invoke on a source skill directory.

New bin/ scripts:

  • bin/install-skill-finder.sh — one-line curl installer for
    skill-finder + skill-publisher into any of three agent runtimes.
  • bin/install-skill-publisher.sh — same, publisher-only.
  • bin/provision.sh — end-to-end customer bootstrap. Enables
    GCP APIs, creates the GCS bucket, sets up the API hub attribute
    taxonomy, generates a fresh Ed25519 signing keypair on the
    operator's machine, publishes the three example skills, and drops
    the trust root into skill-finder's keys/.

The five-minute path for a new customer:

# 1. Install skill-finder + skill-publisher on your machine.
curl -fsSL https://raw.githubusercontent.com/apigee/devrel/main/\
references/apigee-skills-serving/bin/install-skill-finder.sh \
  | bash -s -- --runtime gemini

# 2. Clone the reference and provision your project.
git clone https://github.com/apigee/devrel.git
cd devrel/references/apigee-skills-serving
bash bin/provision.sh --project MY_PROJECT --yes

Trust model (deliberate design)

The apigee/devrel repo intentionally contains no signing key
material and no signed release bundles
. This constraint drove
several design choices:

  • skill-finder itself is trusted by provenance, not by
    signature. TLS + the apigee GitHub org is the trust boundary.
    This matches how kubectl, gh, and terraform installers
    work.
  • Payload skills are trusted by signature against
    per-deployment trust roots. Each customer's provision.sh
    generates a fresh Ed25519 keypair on their machine; the private
    key never leaves the machine and is never committed to any
    repo.
  • Multi-key trust roots (keys/*.pem) support multi-publisher
    orgs and zero-downtime key rotation. A downloaded manifest is
    accepted if its signing_key_id matches ANY installed key.

Full trust-model rationale in
docs/architecture.md.
Customer runbook in
docs/provisioning.md.

Explicit non-goals

  • No signed release bundles attached to apigee/devrel releases.
    release-please's monorepo cadence is unchanged.
  • No signing key in GitHub Actions. Zero signing infrastructure
    in this repo. Every signature happens on the customer's machine.
  • No changes to the existing publisher scripts (pack_skill,
    sign_skill, upload_skill, register_skill,
    update_taxonomy). This PR only adds new consumers of them.
  • No taxonomy additions beyond skill-spec (which provision.sh
    patches into system-spec-type; the same PATCH the demo scripts
    have needed since feat(apigee-skills-serving): add reference for Apigee API hub as agent skill catalog #889). v2 trust-root-in-API-hub design is
    documented but deferred.

Change breakdown

7 commits totalling ~5389 LoC across 20 files:

Commit Type Files Description
3a02054d fix 1 demo-setup.sh env-ordering bug (small carry-over: pre-flight was called before env export)
babed2e6 feat 9 Copies skill-finder + skill-publisher source in; multi-key trust-root code changes in find_install.py
df3ae710 feat 2 bin/install-skill-finder.sh + bin/install-skill-publisher.sh — the one-line installers
e0cbc4fa feat 1 bin/provision.sh — the customer bootstrap script (idempotent across all 7 steps)
2e59ab3f docs 4 New docs/provisioning.md; extends docs/architecture.md trust-model section; updates README.md + docs/publish-and-install.md
362c97a5 fix 2 Bandit + flake8 fixes surfaced by local MegaLinter run
1b4d71f6 test 1 17 new unit tests for the multi-key _load_pubkeys + _verify_signature functions

Verification (all run locally before this PR)

Loop Result
shellcheck on all four new bash scripts PASS
flake8 on skills/skill-finder/scripts/ PASS (0 errors)
pytest full suite 237/237 in 1.4s
pytest full suite as uid=0(root) inside python:3.13-slim (matches Cloud Build env) 237/237
pipeline.sh local run (Cloud Build equivalent) 237/237
MegaLinter v4 via podman (same env vars as .github/workflows/devrel-static-checks.yml) (only non-blocking)
bin/install-skill-finder.sh --repo gsjurseth/devrel --ref feat/add-skill-finder — real fetch + install PASS
bin/provision.sh --check-only against a fully-provisioned project (7/7 steps report OK) PASS
bin/provision.sh wet re-run against fully-provisioned project (idempotent, zero writes, exits at READY) PASS
End-to-end: skill-finder installed via installer, populated with a trust root, resolves + installs a payload skill signed with the matching key PASS (16-line contract)

Two design decisions worth flagging in review

  1. GCS bucket = public-read (allUsers:objectViewer) is a
    demo-only choice. skill-finder currently downloads via anonymous
    HTTPS (https://storage.googleapis.com/<bucket>/<file>.skill),
    no ADC scope. Production would use signed URLs or an
    authenticated consumer service account; adding that would be a
    ~50 LoC change to find_install.py._download_zip. Called out
    in docs/provisioning.md#step-3-gcs-bucket and README
    Limitations.

  2. skill-spec enum patch to system-spec-type is done via
    direct API hub PATCH from provision.sh because
    scripts/update_taxonomy.py only creates user-defined
    attributes (not system-defined). Called out as a known gap in
    docs/provisioning.md#step-4-api-hub-attribute-taxonomy; a
    future PR could migrate the patch into update_taxonomy.py.

Follow-ups (out of scope for this PR)

  • docs/releasing.md — deliberately not added. The reference
    implementation ships with the devrel monorepo's release-please
    cadence; installers fetch main by default. No separate release
    channel to document.
  • v2 API-hub-discovered trust root — documented in
    architecture.md#trust-root-discovery-in-v2 but not
    implemented. Would eliminate the "operator manually populates
    keys/" step by having skill-finder fetch its trust root from
    API hub itself.
  • Antigravity install path verificationinstall-skill-finder.sh
    supports --runtime antigravity (installs to
    ~/.gemini/antigravity/skills/) but I have not tested this
    live (no antigravity runtime available in the dev environment).
    The code path is shared with the Gemini CLI install (tested
    green), so risk is low, but flag if a maintainer has an
    antigravity setup.

gsjurseth added 7 commits July 7, 2026 08:13
demo-setup.sh had an ordering bug: it invoked
bin/check-prerequisites.sh at Step 1 (before the demo env vars
are exported at Step 2). check-prerequisites.sh reads
APIHUB_PROJECT, APIHUB_LOCATION, APIGEE_ORG, and
APIGEE_SKILLS_MIN_KEYWORD_OVERLAP directly from the process
environment. In a fresh shell (the common case -- operator has
just opened a terminal to start the demo), none of those are
set yet, so the checker fails 3/4 required prerequisites and
aborts, even though this script is self-configuring and would
work if allowed to run to completion.

Reordered:

  1. Verify ADC (unchanged - fast, independent of env vars).
  2. NEW: export demo env vars + write persistent config file.
  3. NEW: run pre-flight check AFTER the export, so it sees the
     demo values.

The check-prerequisites.sh call is preserved as the invariant
enforcer for an operator who prefers to source env.sh manually
-- it just no longer spuriously fails when the operator relied
on this script to configure the shell.

Verified in a synthetic empty-env shell via
    env -i HOME=$HOME PATH=$PATH USER=$USER \
        bash bin/demo-setup.sh
which now prints the READY banner instead of aborting on the
3/4 missing-env-vars check.

shellcheck on the modified file continues to PASS.
…s (multi-key trust)

Introduces two new agentic skills in
references/apigee-skills-serving/skills/:

  - skill-finder: consumer-side discovery/install client that
    queries the customer's API hub for signed skill bundles,
    verifies their ed25519 signatures against locally-installed
    trust roots, and installs them into the runtime's skills
    directory (opencode / gemini-cli / antigravity). Ports the
    16-line Hyrum's Law stdout contract from the standalone
    gsjurseth/skill-finder repo.

  - skill-publisher: author-side pipeline orchestrator that
    shells out to the four existing publisher Python modules
    (scripts/{pack,sign,upload,register}_skill.py) in order.

Both skills ship as **source only** -- no signed release
bundles, no key material, no trust-root fingerprints
committed to this repo. The trust model:

  - skill-finder itself is trusted by provenance (the operator
    fetched it from apigee/devrel over TLS). No signature is
    verified on skill-finder because the code that would verify
    it IS skill-finder.

  - Payload skills that skill-finder installs (currency-converter,
    weather-lookup, apigee-policy-top10, and any other skill
    the customer registers) are trusted by signature. Every
    downloaded manifest is verified against the trust roots in
    ~/.<runtime>/skills/skill-finder/keys/*.pem.

  - Trust roots are per-deployment, generated fresh on each
    customer's machine by the (forthcoming) bin/provision.sh
    bootstrap script. No shared signing key crosses this repo.

Multi-key trust root implementation
-----------------------------------

skill-finder now loads EVERY PEM in keys/ as an accepted trust
root, not just a hardcoded keys/trusted_pubkey.pem. A downloaded
manifest is accepted if its signing_key_id matches ANY installed
trust root. This supports:

  - Multi-publisher orgs (Alice's team's key + Bob's team's
    key both trusted simultaneously).
  - Zero-downtime key rotation (new key added, old key removed
    later; new-signed and old-signed manifests both verify
    during the overlap window).
  - Per-project trust bootstrapping (drop a customer's fresh
    ed25519 public key into keys/ during provisioning; skill-
    finder trusts it from the next invocation onward).

Empty keys/ dir is a hard-fail with a clear operator-facing
diagnostic pointing at bin/provision.sh. Duplicate keys under
different filenames warn but proceed. Non-PEM/non-.raw files
under keys/ (README.md, .gitkeep) are skipped silently.

The LOCKED Hyrum's Law contract lines are preserved with a
targeted wording tweak:

  before: "key-id check: trusted (matches embedded fingerprint)"
  after:  "key-id check: trusted (matches installed key <fp>)"

The change surfaces WHICH key matched, useful during rotation
troubleshooting. The "manifest signature: OK (ed25519 / <fp>)"
line is unchanged.

Files
-----

  skills/skill-finder/
    SKILL.md                     runtime instructions + dispatch
                                 (opencode | gemini | antigravity)
    keys/README.md               operator doc: how to populate keys/
    keys/.gitkeep                keeps the dir tracked
    manifest.yaml                source manifest (publisher script
                                 populates signature fields at
                                 publish time)
    scripts/find_install.py      SEARCH + INSTALL mode (multi-key
                                 verify loop; 16-line contract)
    scripts/list_skills.py       LIST mode (catalog browser)

  skills/skill-publisher/
    SKILL.md                     runtime instructions
    manifest.yaml                source manifest
    scripts/publish.sh           pack -> sign -> upload -> register
                                 orchestrator

Verified end-to-end against a real API hub
------------------------------------------

Ran the modified find_install.py in three scenarios against the
geirs-spaces-demo project:

  1. Happy path: one trust root installed, matches the manifest's
     signing_key_id. All 16 contract lines print, exit 0.

  2. Empty keys/ dir: emits the operator-facing "trust root:
     FAILED" diagnostic and exits before any network call.

  3. Wrong-key: one bogus key installed, no match. Emits the
     "key-id check: FAILED — trusts: <list>" line and exits
     with code 3 (integrity failure) before downloading anything.

  4. Rotation scenario: two keys installed (one bogus, one real).
     find_install.py picks the matching key and verifies
     successfully. Confirms rotation-with-overlap works.

License headers
---------------

All Python and bash source files carry the Apache 2.0 header
matching devrel's convention. keys/README.md and manifest.yaml
files carry copyright comments.

Followups (separate commits in this PR)
---------------------------------------

  - bin/install-skill-finder.sh and bin/install-skill-publisher.sh
    (installer scripts adapted from gsjurseth/skill-finder v0.1.7)
  - bin/provision.sh (net-new customer-bootstrap script that
    generates the trust root, provisions the demo, and publishes
    the initial skill catalog signed with the fresh key)
  - docs/{trust-root.md,provisioning.md,releasing.md}
  - README and publish-and-install.md updates
  - tests for the multi-key verify loop
…stall-skill-publisher.sh

Two new installer scripts under
references/apigee-skills-serving/bin/ that let operators install
the skill-finder and skill-publisher agentic skills into their
agent runtime (opencode | gemini | antigravity) in one line:

  curl -fsSL https://raw.githubusercontent.com/apigee/devrel/main/\
      references/apigee-skills-serving/bin/install-skill-finder.sh \
      | bash -s -- --runtime gemini

Trust model
-----------

The installer scripts (and the skill-finder source they fetch)
are trusted by provenance: TLS + the apigee GitHub organisation.
There are no signed release bundles and no sha256 pins baked into
the installers. This differs from a signed-release model
(e.g. gsjurseth/skill-finder's v0.1.7 installer) but matches the
"no signing key ever enters this repo" constraint: apigee/devrel
does not have an organisation-level signing key, and adding one
would require key-management governance that is out of scope for
the reference implementation.

Payload skills that skill-finder subsequently installs from a
customer's API hub ARE cryptographically verified. Those
signatures use per-deployment trust roots that live under
~/.<runtime>/skills/skill-finder/keys/ and are populated by
bin/provision.sh (a subsequent commit in this PR) or manually.

How the fetch works
-------------------

Rather than depending on a `git` binary on the operator's
machine, the installers fetch the entire devrel repository as a
gzipped tarball from codeload.github.com and extract only the
skill directory they need. Three URL forms are attempted in
order (tag, branch, raw SHA) so --ref works for all three.
This is more portable than `git clone --depth 1` and works on
any machine with `curl` and `tar` in the toolchain.

Design choices
--------------

  - Per-user venv at ~/.local/share/skill-finder/venv shared by
    both skills. The venv is the escape hatch for PEP 668 distros
    (Debian 12+, Ubuntu 23.04+, recent macOS Homebrew) where
    system `pip install` is refused. It also isolates skill deps
    from other Python projects on the operator's machine.

  - Per-install `bin/run-with-venv.sh` wrapper. SKILL.md
    invocations of the shipped scripts are rewritten at install
    time from `python3 ${SKILL_DIR}/scripts/X.py` to
    `${SKILL_DIR}/bin/run-with-venv.sh ${SKILL_DIR}/scripts/X.py`
    so the agent's shell tool always uses the venv even when the
    system python doesn't have cryptography / google-auth /
    requests / pyyaml.

  - scripts/common/ is copied into each skill's install at install
    time. skill-finder's scripts do `from common.X import Y` which
    only works if common/ is a sibling of the script. The source
    tree ships find_install.py and list_skills.py alone; the
    installer splices in the common package from the enclosing
    devrel checkout.

  - Runtime auto-detection: OpenCode -> Antigravity -> Gemini
    CLI -> OpenCode fallback. Operator can always override with
    --runtime.

  - Refuses to overwrite an existing install without --force.
    Empty keys/ is a hard-fail with a clear diagnostic pointing
    at bin/provision.sh.

Verified end-to-end
-------------------

Ran the following against gsjurseth/devrel at feat/add-skill-finder
(the branch this PR will become):

  bash bin/install-skill-finder.sh \
    --runtime gemini \
    --repo gsjurseth/devrel \
    --ref feat/add-skill-finder \
    --force

Result: fetched 10.2 MB tarball, extracted the two skill
directories, created a fresh venv, installed 4 deps, wrote
`bin/run-with-venv.sh` wrappers, rewrote both SKILL.md files
to use the wrappers, atomic-mv'd both installs into place,
printed the trust-root-required diagnostic.

Then dropped a fingerprint.pem into keys/ and re-ran
find_install.py against a live API hub: 16/16 contract lines
printed, exit 0, apigee-policy-top10 installed successfully.

shellcheck on both scripts: PASS.
Adds a single end-to-end bootstrap script that provisions an
empty GCP project as an apigee-skills-serving reference
deployment. Given a project with a pre-existing Apigee
organisation and API hub instance, provision.sh:

  1. Enables the four GCP APIs skill-finder + skill-publisher
     need (apigee, apihub, storage, iam).
  2. Verifies the API hub instance + Apigee org exist; fails
     fast with a clear message if not (creating an API hub
     instance takes 40+ min and has console-only decisions --
     out of scope for automation).
  3. Creates a public-read GCS bucket for signed .skill
     bundles (public-read because skill-finder downloads via
     anonymous HTTPS -- demo choice; production would use
     signed URLs or an authenticated consumer).
  4. Creates the four API hub user-defined attributes
     (agentic_skill, keywords, gs_uri, signing_key_id) and
     patches skill-spec into the system-spec-type enum.
     Latter is a known gap: update_taxonomy.py doesn't add
     skill-spec, but register_skill.py requires it.
  5. Generates a fresh ed25519 signing keypair on THIS
     machine. Private key at ~/.config/apigee-skills-demo/
     signing.raw (mode 0600). Public fingerprint printed for
     the operator to record.
  6. Packs, signs, uploads, and registers the three example
     payload skills (currency-converter, weather-lookup,
     apigee-policy-top10) with the fresh key.
  7. Installs the corresponding public PEM into an
     already-installed skill-finder's keys/ directory so it
     will trust the just-published catalog.

Idempotency
-----------

Every step detects existing state before deciding whether to
act. Re-running against a fully-provisioned project produces
seven OK lines and exits at the READY banner without making
any changes:

  Step 1: "apigee.googleapis.com already enabled" x 4
  Step 2: "API hub instance: ...", "Apigee organisation: ..."
  Step 3: "bucket gs://... exists", "public-read enabled"
  Step 4: "user-defined attributes: all 4 present",
          "system-spec-type includes skill-spec"
  Step 5: "existing key at ... (reusing; pass --force ...)"
  Step 6: "already published: ...", "all example skills already
          published"
  Step 7: "trust root already installed: ..."

Modes
-----

  --check-only  Read-only preflight. Reports what's missing
                without touching anything. Useful for
                pre-demo dry-runs.

  --dry-run     Like --check-only but tells you WHAT it would
                do at each step.

  --force       Overwrite an existing signing key (default:
                reuse -- overwriting would invalidate all
                prior signatures).

  --yes         No interactive prompts (for automation).

Selective skips
---------------

  --skip-apis, --skip-bucket, --skip-taxonomy, --skip-skills,
  --skip-key, --skip-trust-root-install

Any subset can be combined. Useful for surgically re-running
a single step or for splitting the initial provisioning from
a later trust-root-only re-run (after installing skill-finder).

Trust model
-----------

The signing keypair is generated on the operator's machine
and never leaves it. There is no shared org-level signing
key; each deployment owns its own root of trust. The public
PEM is written to skill-finder's keys/ directory so
skill-finder verifies signatures against the deployment-local
key. Multiple keys can coexist under keys/ (see the multi-key
feature in commit babed2e) which supports multi-publisher
orgs and zero-downtime key rotation.

Verified end-to-end
-------------------

Ran against geirs-spaces-demo (a live GCP project) in three
scenarios:

  1. --check-only: 7/7 steps report OK. No writes, no
     spurious warnings.
  2. Wet re-run against fully-provisioned project: 7/7 steps
     OK, exits at READY with no writes. Fully idempotent.
  3. --force key regeneration: prompts for confirmation
     (unless --yes), overwrites signing.raw, warns operator
     that prior signatures will no longer verify. Publishing
     step then re-signs and re-registers all skills with the
     new key.

shellcheck on the script: PASS.
…+ provisioning

Updates the reference implementation's documentation set to
cover the new one-line install workflow and the per-deployment
multi-key trust model.

Four files changed:

  README.md
    * New "Quickstart (five minutes)" section pointing at the
      curl | bash install of skill-finder followed by
      bin/provision.sh. Kept the underlying pack/upload/sign/
      register invocation as a secondary "Under the hood"
      block for readers who want to understand what the scripts
      chain together.
    * Repo layout section updated: new bin/ scripts
      (install-skill-finder.sh, install-skill-publisher.sh,
      provision.sh), new docs (provisioning.md), new skills
      (skill-finder, skill-publisher).
    * Renamed "Example skills" section to "Shipped skills" and
      added skill-finder + skill-publisher rows.
    * Limitations section rewritten: per-deployment trust root
      (not shared), key rotation supported (multi-key), demo
      choice of public-read GCS. Removed the "publisher and
      consumer share an Ed25519 trust root" claim -- multi-key
      trust roots eliminate that.

  docs/architecture.md
    * "What the consumer verifies" section expanded to name
      skill-finder as the reference consumer and reference the
      16-step Hyrum's Law contract.
    * New subsection "Per-deployment trust roots" explains why
      the trust root is not baked into the repo, why
      skill-finder is trusted by provenance rather than
      signature, and how the private-key-stays-local model
      works.
    * New subsection "Multi-key trust roots" documents the
      keys/*.pem glob model, the three operator workflows it
      supports (multi-publisher, zero-downtime rotation,
      bootstrap onboarding), and the empty-keys diagnostic.
    * Threat model table gains two rows: compromised publisher
      key mitigation (multi-key add-then-remove rotation) and
      compromised installer over TLS (out of scope for v1; pin
      --ref for defence-in-depth).
    * New subsection "Trust-root discovery in v2" explains the
      forward-compatible design: API hub as trust-root store
      via a well-known attribute. Deferred as v2 to avoid
      taxonomy churn and a first-fetch TOFU chicken-and-egg
      in v1.

  docs/provisioning.md   [NEW, 300 LoC]
    Complete customer-onboarding runbook:
      * Prerequisites (Apigee org + API hub instance -- both
        out of scope for provision.sh because they take 10-40
        minutes and have console-only decisions).
      * "Five-minute path" quick recipe.
      * Step-by-step explanation of what provision.sh does
        internally (all 7 steps, plus the state detection
        that makes each step idempotent).
      * Common workflows: add another publisher, zero-downtime
        key rotation, rebuild a partial deployment, remove a
        deployment.
      * Troubleshooting: seven concrete symptom -> fix rows for
        the most common failure modes.

  docs/publish-and-install.md
    * New introduction points at provisioning.md for the
      five-minute path and explains when to keep reading the
      long form (understanding provision.sh internals,
      publishing your own skill, troubleshooting).
    * Section 11 (consumer-side install) rewritten to describe
      skill-finder's actual 7-step verify-and-install flow
      rather than a hypothetical consumer implementation. Adds
      forward links to architecture.md#multi-key-trust-roots
      and architecture.md#trust-model.
    * Cleanup section rewritten to reflect the split between
      payload skills (removed by demo-cleanup.sh) and
      skill-finder / skill-publisher (persist across demos;
      remove manually or via the full-teardown recipe in
      provisioning.md).

Verified
--------

* Code fence parity: balanced across all 4 files.
* Markdown cross-references: no broken relative links (checked
  every ](path) that isn't http:// or a #anchor).
* No stale references to gsjurseth/skill-finder or to signed
  release bundle URLs.
…kill-finder scripts

Fixes 12 megalinter errors surfaced when running the v4 image
locally against feat/add-skill-finder. All fixes are lint-only;
no runtime behaviour changes.

  bandit (2 errors, blocking)
    * find_install.py:438 `assert self._state is not None`
      -> added `# nosec B101 - defensive invariant`
    * find_install.py:807 `assert gs_uri.startswith("gs://")`
      -> added `# nosec B101 - caller invariant`

    The `# nosec` pattern matches the existing devrel convention
    (see scripts/common/http_retry.py which already uses it).
    Both asserts are defensive invariants, not security checks;
    the caller-side contracts they enforce would fail in later
    lines anyway.

  flake8 (10 errors, blocking)
    * find_install.py:116-120,189: E402 module-level import not
      at top of file. These imports come AFTER a deferred
      import-guard block that catches ModuleNotFoundError from
      cryptography and prints a helpful "run under the venv
      wrapper" message. The E402 is by design.
      -> added `# noqa: E402` comments to the affected lines.
    * find_install.py:178: F401 unused import `http_post_retry`.
      -> removed from both the production and dev-fallback
      import blocks.
    * find_install.py:570: E501 line too long (introduced during
      one of my edits).
      -> broke the set comprehension across three lines.
    * find_install.py:789,1114,1118: F541 f-string missing
      placeholders. These are middle-of-multi-line-string
      literals concatenated by Python's implicit concatenation;
      only the parts with interpolation need the `f` prefix.
      -> dropped the `f` prefix from the placeholder-free lines.
    * list_skills.py:45: F401 unused import `os`.
      -> removed.
    * list_skills.py:195,202: F541 f-string missing placeholders.
      -> dropped the `f` prefix.

Verified
--------

  * `flake8 skills/skill-finder/scripts/`: PASS (exit 0)
  * `pytest`: 237/237 pass (unchanged, no behaviour regressions)
  * megalinter v4 via podman (same env vars as
    .github/workflows/devrel-static-checks.yml): finishes with
    "Successfully linted all files, but with ignored errors"
    (`◬`, non-blocking). Zero `❌` blocking failures.

  The 3 categories that WERE blocking before this commit:
    - CREDENTIALS/secretlint: false-positive scan of a stray
      local .venv/ (not present on CI, and cleaned up
      pre-commit).
    - PYTHON/bandit: fixed by this commit.
    - PYTHON/flake8: fixed by this commit.
17 new hermetic pytest cases covering the two functions that
changed when skill-finder moved from a single-key trust root
(keys/trusted_pubkey.pem, hardcoded path) to a multi-key model
(keys/*.pem, glob load): _load_pubkeys and _verify_signature.

Coverage matrix
---------------

  _load_pubkeys (9 tests)
    * missing keys/ directory              -> hard fail
    * empty keys/ directory                -> hard fail
    * keys/ with only dotfiles             -> hard fail
    * keys/ with only README.md            -> hard fail (suffix filter)
    * single PEM                           -> loads, dict size 1
    * two distinct PEMs                    -> loads, dict size 2
    * duplicate key material under two     -> dedupes with warning
      filenames
    * unparseable PEM alongside good PEM   -> skips bad with warning,
                                              loads good
    * raw 32-byte key (.raw suffix)        -> loads alongside PEMs

  _verify_signature (8 tests)
    * single trust root, matching key      -> verifies OK
    * two trust roots, manifest signed by  -> verifies via A
      key A
    * two trust roots, manifest signed by  -> verifies via B
      key B                                    (order-independent)
    * key-id mismatch (untrusted signer)   -> "key-id check: FAILED",
                                              exits non-zero
    * key-id mismatch error message        -> lists both the declared
                                              fingerprint AND the
                                              trusted set (regression
                                              against vague errors)
    * signature field absent               -> distinct "absent" failure
    * signature field malformed base64     -> distinct "malformed"
                                              failure
    * signature signed by wrong key but    -> "ed25519 verify rejected"
      claims trusted signing_key_id           (theoretical; tests the
                                              crypto rejection path)

Design choices
--------------

  * Tests DO NOT touch the network. Every fixture is in-memory
    or under `tmp_path`. All monkeypatching is done via the
    `find_install` module fixture; no HTTP mocking needed.

  * find_install.py lives at skills/skill-finder/scripts/ rather
    than scripts/. The test file loads it via importlib rather
    than a top-level `import find_install`, so pytest doesn't
    need a custom sys.path entry in conftest.py.

  * `_load_pubkeys` and `_verify_signature` are exercised
    directly rather than through the full find_install main().
    The multi-key logic is self-contained in those two functions;
    testing them in isolation gives a targeted, fast (0.64s)
    verification.

  * Test names read as behavioural specs ("empty_keys_dir_fails",
    "two_valid_keys_first_matches"), matching the style of the
    existing test suite (test_sign_verify_integration.py etc.).

Test count before this commit: 220.
Test count after: 237 (+17). All PASS in 1.4s in-process,
including as `uid=0(root)` inside a python:3.13-slim container
(matches the Cloud Build environment where the previous PR
had a root-vs-user regression).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant