Supply-chain security
This document records genie's software-supply-chain controls: how CI/CD actions are pinned, how published artifacts are signed and attested, the pnpm install policy, and the accepted residual risks. It documents the remediation implemented by PR #210 for the M6-03 re-audit follow-up (GitHub issue #207) and reconciles the release-signing scope of issue #69 (M6-04 / DRO-292).
1. Pinned GitHub Actions
Every third-party action in .github/workflows/ is pinned to a full 40-character commit SHA, with the human-readable version recorded in a trailing # vX.Y.Z comment for provenance and review. Mutable tags (@v4, @main, …) are never used, so a tag being force-moved to a malicious commit cannot silently enter a build.
| Action | Pinned commit | Version |
|---|---|---|
actions/checkout | 34e114876b0b11c390a56381ad16ebd13914f8d5 | v4.3.1 |
actions/configure-pages | 983d7736d9b0ae728b81ab479565c72886d7745b | v5.0.0 |
actions/deploy-pages | d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e | v4.0.5 |
actions/setup-node | 49933ea5288caeca8642d1e84afbd3f7d6820020 | v4.4.0 |
actions/upload-artifact | ea165f8d65b6e75b540449e92b4886f43607fa02 | v4.6.2 |
actions/upload-pages-artifact | 7b1f4a764d45c48632c6b24a0339c27f5614fb0b | v4.0.0 |
pnpm/action-setup | b906affcce14559ad1aafd4ab0e942779e9f58b1 | v4.3.0 |
googleapis/release-please-action | 5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 | v4.4.1 |
softprops/action-gh-release | 3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 | v2.6.2 |
docker/setup-qemu-action | c7c53464625b32c7a7e944ae62b3e17d2b600130 | v3.7.0 |
docker/setup-buildx-action | 8d2750c68a42422c14e847fe6c8ac0403b4cbd6f | v3.12.0 |
docker/login-action | c94ce9fb468520275223c153574b00df6fe4bcc9 | v3.7.0 |
docker/build-push-action | 10e90e3645eae34f1e60eeb005ba3a3d33f178e8 | v6.19.2 |
sigstore/cosign-installer | 6f9f17788090df1f26f669e9d70d6ae9567deba6 | v4.1.2 |
.github/workflows/runner-guard.yml uses no third-party actions (inline run: steps only), so it has nothing to pin.
Updating a pin
To bump an action, resolve the new release tag to its commit SHA (for annotated tags, resolve to the tag's target commit, not the tag object), then update both the @<sha> ref and the # vX.Y.Z comment together. Review the upstream changelog for the range being adopted before merging.
2. Published-artifact signing and attestation
.github/workflows/release.yml publishes four artifact classes, each with its own integrity evidence. Before release-please creates any tag or draft GitHub Release, one preflight checks public repository visibility plus npm and Docker Hub credentials; it then authenticates to npm, confirms that account's ambitresearch organization membership, lists the scope package access visible to the token, and performs a Docker Hub login before release creation. These non-destructive probes catch missing or revoked credentials and npm membership mistakes, but cannot prove registry write scopes without mutating a package or image. The publish jobs remain the authoritative authorization check:
- npm packages (
@ambitresearch/genie,@ambitresearch/genie-viewer): each package is packed once, signed keyless withcosign sign-blob, verified locally against this repository'srelease.ymlidentity and signing-workflow SHA, and published from that exact.tgzwithnpm publish --provenance --access public. A separate pre-publish gate proves the release tag targets the CI-tested SHA. The tarball and its Sigstore bundle (.tgz.sig) are attached to the package's GitHub Release. npm provenance produces a signed SLSA attestation via GitHub OIDC (id-token: write), linking the published tarball to the source commit and workflow run. Provenance requires the source repository to be public before the first live publish. - npm SBOMs: a CycloneDX JSON SBOM is generated for each package with
@cyclonedx/cdxgen(a lockfile-pinned, integrity-hashed devDependency invoked byscripts/generate-package-sbom.mjs; it reads the rootpnpm-lock.yamlonce in non-recursive, no-install mode, fails if any dependency extractor errors, then re-roots the unfiltered graph to the selected package's manifest-declared runtime dependency closure; the wrapper restores pnpm optional-dependency edges that cdxgen emits as components without graph links), signed and verified with cosign, and attached to that package's GitHub Release with its.sigbundle (genie-server-sbom.cdx.json,genie-viewer-sbom.cdx.json). The SBOM is generated beforenpm publish(so a tooling failure surfaces before the irreversible publish) and written to a workspace-levelartifacts/directory, never inside the package, so it cannot be swept into the published npm tarball. The wrapper fails if the package identity/version, any direct runtime dependency, or the transitive closure is missing, and runs cdxgen's strict schema/deep validator before signing.cdxgenis used instead ofcyclonedx-npmbecause this is a pnpm workspace withworkspace:*specifiers and nopackage-lock.json, which the npm-lock-based generator cannot parse. - Container images (GHCR + Docker Hub): built multi-arch (amd64/arm64) from the CI-verified release tag.
docker/build-push-actionis configured withsbom: trueandprovenance: mode=max, so each pushed image manifest carries an embedded SPDX SBOM and a max-detail SLSA provenance attestation. Each image digest is then pushed under a unique, unpromoted staging tag, signed and locally verified keyless with cosign using GitHub OIDC, and only then promoted to the release version andlatesttags. Promotion metadata must report the exact signed digest. No long-lived signing key is stored. - Claude Desktop bundle (
genie.mcpb): built from the release tag, signed and locally verified keyless, then attached to the server GitHub Release with itsgenie.mcpb.sigbundle.
Release Please deliberately creates draft component releases. Signed blobs and SBOMs attach to those drafts before npm publication; container jobs then read back each promoted version tag and require its digest to match the signed build digest. The finalizer fails closed unless every applicable job succeeded, every expected asset is attached, and the exact npm versions are live. Only then does it publish the GitHub Releases. This sequencing prevents GitHub's immutable-release setting from locking an empty release before its assets arrive.
If a production attempt publishes npm and attaches the signed blobs but a container registry fails, dispatch the Release workflow manually from main with the existing server and viewer draft tags. This guarded recovery is not a second package-publish path: it refuses non-draft, immutable, mismatched, historical, or non-main inputs; never runs npm publish; and never uploads or replaces release assets. Normal production and recovery runs share one non-cancelling lock, and both input versions must still match npm latest before registry tags can move. It rebuilds and signs both registry images from the exact server tag, requires amd64 and arm64 plus promoted-tag digest equality, then downloads and cryptographically verifies every existing blob bundle and checks both npm SLSA provenance records. Registry signature and promoted-tag reads retry for at most 60 seconds after the single write, without relaxing certificate identity, workflow SHA, or exact-digest equality. Both drafts are published only after the two independent registry jobs pass. See Recovering an incomplete production release for the operator procedure.
Verifying a release
- npm provenance:
npm audit signaturesafter install, or inspect the provenance attestation on the package's npmjs.com page. - Release blobs: download an artifact and its
.sigbundle, then runcosign verify-blob --bundle <artifact>.sig --certificate-identity 'https://github.com/ambitresearch/genie/.github/workflows/release.yml@refs/heads/main' --certificate-oidc-issuer https://token.actions.githubusercontent.com <artifact>. - Container signature:
cosign verify <image>@<digest> --certificate-identity 'https://github.com/ambitresearch/genie/.github/workflows/release.yml@refs/heads/main' --certificate-oidc-issuer https://token.actions.githubusercontent.com. - Image SBOM/provenance:
docker buildx imagetools inspect <image> --format '{{ json .SBOM }}'and… '{{ json .Provenance }}'. To extract the in-toto SPDX predicate:cosign download attestation --predicate-type https://spdx.dev/Document --platform linux/amd64 <image>@<digest> | jq -r '.payload' | base64 --decode | jq '.predicate'.
3. pnpm install policy
pnpm-workspace.yaml sets:
blockExoticSubdeps: true— rejects non-registry transitive dependencies (git, tarball, http, or link specifiers pulled in by a subdependency). All first-party internal links areworkspace:*direct dependencies, which remain allowed; only exotic subdeps are blocked. This is the pnpm 11 default, set explicitly so the policy is also pinned under the pnpm 10.x line the repo currently uses.minimumReleaseAge: 10080— a 7-day cooldown that refuses to install any published version younger than 10080 minutes. This blunts the "publish a malicious patch and hope a build pulls it before takedown" window on fresh installs and lockfile updates. It does not apply to--frozen-lockfileinstalls, which every CI leg uses, so reproducible CI cannot break from the cooldown. Because the cooldown is enforced during resolution, regenerating the lockfile while an already-locked or newly-selected version is <7 days old will fail until that version matures; a maintainer bumping dependencies inside the window can add the specific package tominimumReleaseAgeExclude(below) or pass a one-off--config.minimumReleaseAge=0for a reviewed lockfile update. Docker's legacypnpm deployalso uses that command-local override because legacy deploy performs a resolution pass; it simultaneously requires--frozen-lockfile, so no version can differ from the reviewed lockfile.minimumReleaseAgeExclude— exempts the@cline/cli-*platform binaries from the cooldown. They are an exactly-pinned, dev/test-only E2E dependency (packages/e2e) that tracks a fast-moving upstream and never ships in a published artifact, so the cooldown adds no supply-chain value there while otherwise blocking routine lockfile maintenance. Every other dependency still honoursminimumReleaseAge.trustPolicy: no-downgrade— fails resolution if a package release has weaker registry trust evidence than an earlier-published release. This policy is supported since pnpm 10.21 and is enforced by the repository-pinned pnpm 10.34.4.
package.json also pins external-editor>tmp to fixed tmp@0.2.7 and sets pnpm.overrides.typescript: "$typescript". The former closes GHSA-52f5-9888-hmc6, GHSA-ph9p-34f9-6g65, and GHSA-7c78-jf6q-g5cm in the MCPB packaging toolchain. cdxgen pulls in typescript as an optional transitive dependency (via @appthreat/atom-parsetools); without the override pnpm re-binds the workspace's optional typescript peers (notably vue) onto that newer version. The override pins the entire tree to the root typescript devDependency spec, so adding the SBOM tool does not silently change the TypeScript version the runtime packages build against.
4. Dependency vulnerability scanning
The lockfile is scanned with OSV Scanner (osv-scanner scan source -r .); the last recorded scan reported no known vulnerabilities across the lockfile packages. pnpm audit --prod is used opportunistically, but the npm audit advisory endpoint has intermittently returned HTTP 410, so OSV Scanner is the primary gate.
CI also runs Gitleaks 8.30.1 from a digest-pinned OCI image against full Git history (fetch-depth: 0). .gitleaks.toml extends the default rules and permits only five exact deterministic fixture values or extracted fixture identifiers. It contains no path, commit, stopword, or rule-wide exclusions, so another secret in the same test files still fails the gate.
5. Accepted residual risks
- Tool-output trust boundary:
conjurereturns model-controlled content. It is treated as untrusted data at the MCP host boundary — never concatenated into privileged prompts, and persisted only through the separate plan-guarded write flow. See the public security model. postMessage("*")in the viewer: the embedded viewer posts MCP-App protocol notifications to its parent withtargetOrigin: "*". No file contents, secrets, or generated payloads are sent — only initialize/size/ping/teardown protocol data. Pinning the parent origin is deferred until the MCP-Apps host contract exposes one reliably.
References
docs/developer/security.md— public security model and reporting boundary.- GitHub issue #207 / PR #210 — supply-chain remediation documented here.
- GitHub issue #69 (M6-04 / DRO-292) — release signing scope reconciled here.