Commit Graph
5 Commits
Author SHA1 Message Date
Evan Hu 786cbd496a chore(ci): harden refresh-cache workflow per PR re-review CRITICAL #1
The first iteration moved signing from the Cloudflare worker into this
repo's CI to fix the worker-as-sign-oracle defect. The re-review pointed
out that this just relocated the trust problem: anyone who lands a
commit on main gets the resulting bytes signed automatically. Mitigations:

* CODEOWNERS — sign-script, build scripts, the workflow itself, and
  the auto-generated index/sig files are owned by the registry owner.
  Combined with branch protection requiring CODEOWNERS review, a PR
  touching the signing infrastructure or the artefacts it produces
  cannot land without explicit owner sign-off. Plugin contributions
  under plugins/<name>/ are covered by the standard PR-review rules
  but don't trip CODEOWNERS unless they touch the signing path.

* SHA-pinned actions — actions/checkout@v4 and actions/setup-node@v4
  replaced with full-SHA refs (v4.2.2 / v4.1.0). Blocks
  action-supply-chain swaps (a malicious mutable tag move on a
  popular action would otherwise execute in the same job that has
  REGISTRY_PRIVATE_KEY in scope).

* Post-sign self-verify — a new step verifies plugins-index.json.sig
  against the committed pubkey (not a secret) BEFORE the .sig hits
  main. Catches a buggy sign-script run, an env-leak that produces
  zero-bytes output, or a half-applied edit.

* REGISTRY_PRIVATE_KEY env scope — explicitly noted in workflow
  comment that the secret is on the sign step ONLY (where it was
  already), not the job. Prevents future contributors lifting it
  to job-level out of convenience.

* In-workflow security model docstring — enumerates the residual
  threat model (compromised maintainer, malicious PR, sign-step
  bug, leaked refresh token) and what each defense addresses.

Trust root is now: GitHub branch protection on main + CODEOWNERS on
sign infrastructure + SHA-pinned actions + post-sign verification.
A maintainer with push rights can still ship malicious bytes through
a code-review bypass — that residual is the same as for any signed
package registry and falls outside what CI alone can mitigate.

Branch protection on `main` MUST be configured by an org admin to
match the assumptions in CODEOWNERS:
  - require pull request reviews (at least 1)
  - require review from CODEOWNERS
  - dismiss stale approvals on new commits
  - restrict who can push directly to main (org admins only)
2026-05-05 01:18:46 +09:00
Evan Hu 74745f1f20 ci: sign plugins-index.json in-repo, drop worker signing dependency
Pair with the worker-side simplification on the librefang PR — the
worker is now a pure transport (no key material, no signing) and this
repo's CI takes over signature production.

scripts/sign-plugins-index.mjs reads REGISTRY_PRIVATE_KEY from a GitHub
Actions secret, signs plugins-index.json with Ed25519, and writes
plugins-index.json.sig alongside it. Aborts loudly when the secret is
missing so a misconfigured CI can't silently ship an unsigned payload.

The workflow now runs build → sign → commit (.json + .sig) → push →
poke worker /refresh. The worker fetches the committed .json + .sig
verbatim and stores both — the daemon then verifies against the
embedded pubkey it ships with.

Closes PR review CRITICAL #1: the worker is no longer a sign-anything
oracle reachable via REGISTRY_REFRESH_TOKEN. Trust root is now this
repo's branch protection + Actions secret scope, not a token any CI
job that can talk to stats.librefang.ai can use to mint signatures.

Note: the keypair was rotated as part of this change (PR not yet
merged so no daemon TOFU pins exist). New pubkey:
  ClGa0Ucap8NdrKAy1rw9Tt6A9I8eg4zJ53+xIuKMuq0=
The plugins-index.json.sig committed here is signed with the matching
new private key, in lockstep with the daemon EMBEDDED_REGISTRY_PUBKEY
constant and all three worker [vars] entries.
2026-05-05 01:02:58 +09:00
Evan Hu ff6f3f2b25 ci: build registry-index.json in-repo for dashboard real-time updates
Pair with the existing plugins-index.json path (which feeds the
daemon's signed install lane). registry-index.json mirrors the
dict-shaped payload the registry-worker's cron currently builds via
40+ GitHub Contents API calls — but built locally from the checked-out
tree by scripts/build-registry-index.mjs, so the worker only fetches
ONE file per category type (2 total: plugins + registry) on refresh.

Workflow now picks up content changes across all 8 category dirs
(was: plugins/ only) so dashboard updates land within seconds of a
push instead of waiting for the 02:00 UTC cron tick.

The dashboard's /api/registry endpoint reads kv_store('registry_data');
the worker's forced-refresh now writes that key with these bytes and
purges the Cache-API entry, so the next dashboard hit sees fresh
data instead of the 1h-cached previous payload.

Generated counts on first build: 11p 17h 32a 60s 44c 57pr 22w 33mcp.
2026-05-05 00:43:30 +09:00
Evan Hu f9821d5867 ci: build plugins-index.json in-repo so worker refresh stays under budget
Walking ~40+ plugin TOMLs via the GitHub Contents API from the worker
exceeded the Workers Free 50-subrequest-per-invocation limit, leaving
the daemon's signed plugins index either empty or partial after every
forced refresh.

Move the walk into the repo: scripts/build-plugins-index.mjs reads each
plugins/<name>/plugin.toml directly from the checked-out tree and emits
a sorted flat array (name, version?, description?, needs?) at
plugins-index.json. The CI workflow regenerates and commits this file
on every push under plugins/, then pokes the worker's
/api/registry/refresh — which now fetches the single committed
plugins-index.json (1 subrequest), validates the JSON shape, and
re-signs it with Ed25519. Refresh cost is now constant in registry
size, not linear.

The dashboard's dict-shaped /api/registry payload is unchanged — that
still rebuilds via the daily 02:00 UTC cron.
2026-05-05 00:37:50 +09:00
Evan Hu 14a576671d ci: trigger registry-worker refresh on push to main
Without this, dashboard / daemon see content changes only after the
next 02:00 UTC cron tick (up to ~24h delay). The action POSTs to
stats.librefang.ai/api/registry/refresh with a bearer token shared
with the worker secret of the same name; until both secrets exist on
their respective sides, the endpoint returns 503 and the action fails
loud — no silent half-deploy.

Filters on directories the worker actually reads (plugins/, agents/,
skills/, hands/, channels/, providers/, workflows/, mcp/) so README
edits don't burn worker invocations.

Known limitation: the worker rebuild walks ~40+ GitHub Contents API
subrequests, which on Workers Free truncates partway through and
leaves some categories empty. This action fires the right path; the
underlying budget fix (Workers Paid, or pre-building the index in
this repo) is tracked separately.
2026-05-05 00:35:04 +09:00