// Package hooks is spec.sr.ht's receive path: the git hooks a space's bare
// repository runs on every push, and the daemon-side RPC endpoint they call.
//
// # Why the hooks are service code
//
// bleve is single-writer and the running specsrht daemon holds the index open,
// so a separate hook process cannot reindex; and validation needs the same
// schema and policy logic the API uses, which must not be duplicated in a
// shell script that will drift from it. Both hooks are therefore thin shims
// that RPC into the daemon over a unix socket, calling [service.Service]
// entry points. Nothing in this package shells out to git.
//
// Daemon availability is consequently part of the push path, so the behaviour
// is stated rather than discovered: **fail closed**. If the daemon is
// unreachable, unresponsive, or answers anything this package cannot parse,
// the push is rejected. A rejected push is recoverable in one command; a
// silently unvalidated, unindexed one is a corruption discovered weeks later.
//
// # Which hook does what
//
// Three hooks are installed, not two, and the reason is a property of git that
// the design did not account for. Both facts below were verified against
// git 2.55 rather than inferred from the documentation:
//
// 1. GIT_PUSH_OPTION_COUNT / GIT_PUSH_OPTION_<n> are set for `pre-receive`
// and `post-receive` only. githooks(5) documents them under exactly those
// two hooks, and `update` observably runs without them. So `update` alone
// cannot see --push-option=skip-validation.
// 2. During `pre-receive` the pushed objects are still in receive-pack's
// quarantine directory (GIT_QUARANTINE_PATH), reachable only through the
// hook process's own GIT_OBJECT_DIRECTORY. A separate process that opens
// the bare repository — the daemon — cannot read them. git migrates the
// quarantine into the real object store immediately after `pre-receive`
// succeeds and before the first `update` runs, so `update` is the earliest
// hook at which the daemon can read what is being pushed.
//
// Neither hook can do the whole job, so the work is split along that seam:
//
// - pre-receive — the only place push options exist. It forwards them, with
// the full list of proposed ref updates, to the daemon, which records them
// for the duration of this push. It validates nothing (it cannot: the
// objects are invisible to the daemon) but it is still a rejecting hook,
// because a daemon that cannot be reached must stop the push here rather
// than one hook later.
// - update — per ref, before the ref moves, and the only hook whose refusal
// is scoped to a single ref. This is where the refs rule and frontmatter /
// document-id validation run, via [service.Service.ValidatePush].
// - post-receive — after the fact, cannot reject. It tells the daemon the
// push landed so the space can be reindexed and its index rev stamp moved.
//
// The two phases of one push are correlated by (repository, receive-pack pid):
// every hook of a single push is a direct child of one receive-pack process,
// so os.Getppid() is stable across them, and the daemon additionally requires
// the ref update `update` presents to appear in the list `pre-receive` sent.
// An `update` call with no recorded pre-receive phase is refused rather than
// assumed unskippable — that combination means either a broken install or a
// daemon restart mid-push, and both deserve a message instead of a guess.
//
// # Push options
//
// The one recognised option is `skip-validation`. It waives frontmatter and
// document-id validation and nothing else; the refs rule is never skippable.
// An unrecognised push option is a rejection, not a no-op: with exactly one
// option in the vocabulary, a silently ignored `--push-option=skip-validaton`
// would present as an inexplicable rejection of a push the human believed they
// had waived.
//
// Push options only reach a hook when the repository advertises them, so
// [Install] sets receive.advertisePushOptions on every repository it installs
// into. Without it `git push --push-option=...` fails client-side with
// "the receiving end does not support push options".
//
// # Identity
//
// The hook does not decide who is pushing; it forwards a credential and the
// daemon resolves it:
//
// SPECSRHT_PRINCIPAL "owner" or "agent" (required)
// SPECSRHT_AGENT_TOKEN agent secret (required when kind=agent)
// SPECSRHT_AGENT agent identity string (provenance, optional)
// SPECSRHT_AGENT_SESSION agent session id (provenance, optional)
//
// An agent's token is validated against the database on every push, so an
// agent credential asserts nothing by itself. `owner` is different: it is an
// assertion, trusted because sshd already authenticated the SSH key and the
// forced-command wrapper — which owns this environment — is what sets it.
// That wrapper is therefore part of the trust boundary: sshd must not
// AcceptEnv any SPECSRHT_* name, or a client could name its own principal.
// A missing or unrecognised SPECSRHT_PRINCIPAL is refused; there is no
// anonymous write path and nothing to default to.
//
// # Addressing
//
// A hook knows which repository it is running in (git chdirs into it and sets
// GIT_DIR) and nothing else, so both the socket and the space are derived from
// that one fact:
//
// - the socket is <repos>/.specsrht/hook.sock, where <repos> is the parent of
// the parent of the repository directory — the layout gitx.DiskPath
// defines. It lives under `repos` rather than `cache` because `cache` is
// documented as safe to delete at any time, and deleting the socket would
// take the push path down until the daemon restarted. SPECSRHT_HOOK_SOCKET
// overrides it for a non-standard deployment or a test.
// - the space is not sent as a name at all. The hook sends the absolute
// repository path and the daemon resolves it against its own configured
// repos root, refusing anything that is not exactly
// <repos>/~<owner>/<name>. A hook can therefore only ever address a
// repository the daemon already owns.
//
// # Installation
//
// [Install] writes each hook as a symlink to the specsrht binary, which
// dispatches on the name it was invoked as ([ModeFromArgs]). There is no shell
// stub, no generated script, and nothing to regenerate when the socket path or
// the protocol changes; refreshing a repository's hooks after an upgrade is
// running [Install] again, which the daemon does for every space at startup.
package hooks