Contributing to Farrier

Thank you for considering it. This document is short, and every rule in it has a reason attached, because rules without reasons get argued with.

Please read docs/SECURITY.md before proposing anything that touches the agent, the intent catalogue, the root helpers, or the policy file. It is not long, and it will save you from writing a pull request that has to be declined on principle.


The three rules

1. Sign off every commit (DCO)

Farrier uses the Developer Certificate of Origin. There is no CLA. Pegasus Networks holds no rights over your contribution that you do not also hold.

Add a sign-off line to every commit:

Signed-off-by: Jane Doe <jane@example.com>

git commit -s does it for you. The name and email must be real and must match the commit author.

This is checked in CI and the check is not overridable. Not because we enjoy blocking pull requests, but because it is the mechanism that makes the licence permanent: under DCO, relicensing Farrier would require the agreement of every contributor, which means no future owner of this repository can take it proprietary — including us. For a security tool whose whole value is that you can verify its claims yourself, that permanence is worth more than the flexibility it costs.

To fix a missing sign-off:

git commit --amend -s --no-edit          # the last commit
git rebase --signoff origin/main         # a whole branch

2. English, everywhere

Identifiers, comments, documentation, commit messages, issue titles, pull request descriptions, UI strings, log messages, error text. All of it.

Pegasus Networks is a German company and this rule costs us something. We keep it because a project that asks strangers to audit its security claims cannot also ask them to read German first. If the argument for a mechanism is only comprehensible to people who speak the maintainers' language, it is not really an open claim.

3. Every type and every function carries a doc comment saying what it does and why it exists

Exported or not. Yes, unexported helper functions too.

The rule of thumb, and the thing reviewers actually check:

If the comment would still be true after you deleted the function, it is not a comment about why the function exists.

// BAD — restates the signature. Deleting the function would not make this false.
// Sign signs the payload.
func (s *fileSigner) Sign(ctx context.Context, payload []byte) ([]byte, error)
// GOOD — what it does, then why this code is here at all.
//
// Sign produces a detached Ed25519 signature over the canonical job payload.
//
// It exists as a backend separate from pkcs11Signer because most operators start with
// a key file and only move to hardware once the fleet justifies the ceremony. Refusing
// that path would not make anyone buy a token; it would push them to keep the key on
// the control plane instead, which is strictly worse.
func (s *fileSigner) Sign(ctx context.Context, payload []byte) ([]byte, error)

Enforcement is split honestly between machines and humans:

Check Mechanism
Comments on exported Go declarations golangci-lint, revive's exported rule
Comments on unexported Go declarations tools/doccheck, an AST walker this repository ships, because no off-the-shelf linter does this
TypeScript, including private members ESLint jsdoc/require-jsdoc with checkPrivate
Sentence shape (starts with the name, ends with a period) revive and godot
Whether the "why" is real Code review. No linter can check it, and reviewers reject // Sign signs the payload. the same way they reject an untested branch

Run make lint before you push. In a repository this young the discipline is nearly free; retrofitting it later is miserable, which is why it is in place before there is much code to comment.


Before you open a pull request

make lint        # golangci-lint, doccheck, ESLint
make test        # unit tests
make guarantee   # the tests that enforce docs/SECURITY.md §1

make guarantee is a required check with no maintainer override. It is what keeps the guarantee true across contributors who never read the design brief — including future maintainers, including us on a bad day.

Things that will be declined, with the reason written down in advance

None of these are a judgement about you or your use case. They are structural, and the reasoning is in docs/SECURITY.md and docs/EXTENDING.md:

If you have a real operational problem behind one of these, open a discussion describing the problem rather than a pull request implementing the mechanism. The usual outcome is a new typed intent with validated parameters, which is a normal reviewable change. The process for that is in docs/EXTENDING.md.

Adding a platform, a collector, a signing backend, a notification sink

These are the seams that are open by design. See docs/EXTENDING.md. The rule throughout is add an implementation, never edit a switch — if your change requires modifying a type switch in the core, that is a missing seam and a legitimate bug report.

If you add a collect.Platform, say explicitly in the pull request what your implementation does about each of the four known silent-wrong-answer traps listed in EXTENDING.md. All four fail quietly rather than loudly, so "it worked on my machine" does not detect them.

Style

Commits and pull requests

Reporting security issues

Do not open a public issue. Use GitHub's private advisory flow, described in docs/SECURITY.md §9.

Code of conduct

By participating you agree to the Code of Conduct.