~bigbes/sr-ht-spec

ref: 7c82786681409f246f2f77110d659fa422267959 sr-ht-spec/authn/doc.go -rw-r--r-- 5.0 KiB
7c827866 — bigbes docs: align provenance section with the one-token decision 27 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// Package authn answers one question for spec.sr.ht — "who is making this
// request?" — and builds the git provenance that answers "who made this write?"
// forever after.
//
// The design has exactly two principals that carry authority:
//
//   - the human owner, recognised by the shared `sr.ht.unified-login.v1`
//     cookie carrying the instance's [sr.ht] owner-name;
//   - an agent, recognised by an opaque bearer token stored (hashed) in the
//     agent_token table.
//
// Everything else is anonymous. Single-user does not mean "no authorization";
// it relocates it onto agents, which is why Principal distinguishes those two
// and nothing finer. Per the confirmed v1 decision there is one agent token and
// no per-space scoping: the boundary that matters is the refs rule (agents may
// only write proposals/*), and that lives in gitx.
//
// The two credentials are deliberately asymmetric:
//
//   - A cookie that is missing, forged, expired or unreadable yields an
//     anonymous principal and never an error. Browsing must keep working.
//   - A bearer token that is present but unknown, revoked or corrupt is a hard
//     failure. An agent that presented an explicit credential must not be
//     silently downgraded to a reader; it would then fail confusingly at the
//     write instead of clearly at the door.
//
// Provenance is the other half. Agent identity and session ID are mandatory on
// every agent write, and are recorded in the commit itself so that a plain
// `git log` on any clone carries the audit trail:
//
//	Author:    claude-code/spec-writer (for bigbes) <agent@spec.srht.bigb.es>
//	Committer: bigbes <bigbes@gmail.com>
//
//	    Add storage model section
//
//	    X-Agent-Session: 8fb9c9a4-b078-4af1-89eb-d97c522f9921
//	    X-Agent-Base: 1f0c1d1a1e2b3c4d5e6f708192a3b4c5d6e7f809
//
// A write missing either field is rejected rather than defaulted: a commit
// stamped with a guessed session is worse than no commit, because it launders
// unattributable output as attributed.
//
// This package owns no storage. The agent_token table lives in db/, which is
// injected through the TokenStore interface declared here — authn never imports
// db, so the dependency arrow keeps pointing downward.
package authn

import "errors"

// Sentinel errors. Callers compare with errors.Is. The split that matters is
// permanent (the credential is bad — 401/403) versus transient (the store could
// not answer — 503); IsAuthFailure draws it.
var (
	// ErrNoToken is returned when a bearer credential was expected but the
	// request carried no Authorization header, or one in another scheme.
	ErrNoToken = errors.New("no agent token presented")

	// ErrUnknownToken is the contract a TokenStore must honour: it is what
	// LookupAgentToken returns (possibly wrapped) when no row matches the
	// presented hash. Any other error is treated as transient, so a Postgres
	// outage reads as "try again", never as "your token is bad".
	ErrUnknownToken = errors.New("unknown agent token")

	// ErrInvalidToken marks a presented credential that is malformed, or a
	// stored row whose hash does not actually match what was presented.
	ErrInvalidToken = errors.New("invalid agent token")

	// ErrRevokedToken marks a token that resolved to a real row which has been
	// revoked. Distinct from ErrUnknownToken so operators can tell "you are
	// using a token I deliberately killed" from "that token never existed".
	ErrRevokedToken = errors.New("revoked agent token")

	// ErrNotAgent is returned when agent provenance is demanded of a principal
	// that is not an agent — the human push path builds no trailers.
	ErrNotAgent = errors.New("principal is not an agent")

	// ErrMissingProvenance marks an agent write that omits the agent identity,
	// the session ID, or the base revision. Mandatory on every agent write; the
	// design is explicit that these are not defaultable.
	ErrMissingProvenance = errors.New("missing agent provenance")

	// ErrInvalidProvenance marks provenance whose values are present but
	// unusable: control characters or angle brackets that would forge a git
	// signature line or inject an extra trailer, an over-long field, or a base
	// revision that is not a hex object name.
	ErrInvalidProvenance = errors.New("invalid agent provenance")

	// ErrMissingConfig is returned by InstanceFromConfig when the instance
	// config lacks a key the provenance identities are built from. It is a
	// startup failure, not a request failure.
	ErrMissingConfig = errors.New("missing instance config key")
)

// IsAuthFailure reports whether err is a permanent credential failure — the
// caller should answer 401/403 — as opposed to a transient backend failure,
// which should answer 503 and be retried. Everything not in this set is
// transient by definition, which is the fail-closed direction: a store outage
// never reads as a valid credential.
func IsAuthFailure(err error) bool {
	return errors.Is(err, ErrNoToken) ||
		errors.Is(err, ErrUnknownToken) ||
		errors.Is(err, ErrInvalidToken) ||
		errors.Is(err, ErrRevokedToken)
}