~bigbes/sr-ht-dolt

ref: dc918296868d42d3c1824561416aedcb87ab1f89 sr-ht-dolt/cmd/doltsrht/mcp.go -rw-r--r-- 5.4 KiB
dc918296 — Eugene Blikh web: link issue ids to the tracker that owns them 5 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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package main

import (
	"context"
	"log/slog"
	"os"

	"github.com/vaughan0/go-ini"

	"go.bigb.es/auxilia/culpa"

	"sourcecraft.dev/bigbes/sr-ht-ecore/bearer"
	"sourcecraft.dev/bigbes/sr-ht-ecore/instconf"

	"sourcecraft.dev/bigbes/sr-ht-dolt/authn"
	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
	"sourcecraft.dev/bigbes/sr-ht-dolt/mcpsrv"
	"sourcecraft.dev/bigbes/sr-ht-dolt/web"
)

// mcpRoute is where the MCP surface answers (docs/DESIGN.mcp.md §3). One exact
// path and not a subtree: the streamable transport is a single endpoint, which
// is why mountRoutes registers it with Handle rather than Mount.
const mcpRoute = "/mcp"

// tokensSection is the section tokens.sr.ht occupies in the shared config.ini.
// Its origin is the whole configuration of the working-token plane; there is no
// key of ours that enables or disables /mcp (docs/DESIGN.mcp.md §10).
const tokensSection = "tokens.sr.ht"

// mcpBrowseOpener satisfies mcpsrv.BrowseOpener over browse.Open, the way
// web.BrowseAdapter satisfies web's own — the same *browse.DB answers both
// method sets, and each package declares the seam it consumes.
//
// It lives here rather than in mcpsrv/ because a package that named browse.Open
// itself could not be driven over fakes, which is the whole point of the seam.
type mcpBrowseOpener struct{}

var _ mcpsrv.BrowseOpener = mcpBrowseOpener{}

func (mcpBrowseOpener) Open(ctx context.Context, diskPath string) (mcpsrv.BrowseSession, error) {
	dbh, err := browse.Open(ctx, diskPath)
	if err != nil {
		return nil, err
	}
	return dbh, nil
}

// newMCPServer assembles the MCP surface over the seams the daemon already has:
// the metadata store through web's request-scoped adapter — the one every browse
// handler reads through, so both surfaces answer one question one way — and the
// bare-store reader above.
//
// It is built before the router and its failures are fatal. An origin that names
// no host leaves /mcp with nothing to guard itself with (mcpsrv.New), and a
// [tokens.sr.ht] section that does not parse is an operator's typo rather than a
// plane to drop quietly: both are boot failures, not requests answered 500 later.
//
// The one thing that is not a failure is the absence of that section. It is
// reported once here, at the level an operator reads, so that "this instance
// refuses working tokens" is a startup line rather than something deduced from
// the first 401 an agent reports.
func newMCPServer(conf ini.File, cfg settings) (*mcpsrv.Server, error) {
	validator, err := newBearerValidator(conf)
	if err != nil {
		return nil, err
	}
	if validator == nil {
		slog.Warn("no tokens.sr.ht origin is configured; /mcp serves anonymous and meta-PAT callers and refuses every working token",
			"component", "mcp", "key", "["+tokensSection+"] origin")
	}

	return mcpsrv.New(web.DBAdapter{}, mcpBrowseOpener{}, validator, cfg.origin)
}

// newBearerValidator builds the tokens.sr.ht working-token validator, or nil
// when this instance runs no such daemon.
//
// The return type is the interface and not *bearer.Validator, and that is
// load-bearing rather than a style: a nil *bearer.Validator handed to
// authn.ResolveBearer as an InstanceValidator is a *typed* nil, which is not the
// contract that function documents and which panics on the first working token
// presented. Returning the interface makes the absent plane a genuinely nil one.
//
// A missing section is a supported configuration and not a degradation
// (docs/DESIGN.mcp.md §10): meta PATs and anonymous callers keep working, and a
// working token is refused because a credential this instance cannot verify is
// refused rather than guessed at. Refusing to boot instead would turn "agents
// cannot authenticate" into "the service is down", on an instance that may
// deliberately run no tokens.sr.ht at all.
//
// The origin is read in its internal form — [tokens.sr.ht] internal-origin
// falling back to origin, which is what instconf.InternalOrigin means — so the
// revocation check of the tokens SPEC ch. 6 step 4 goes container to container
// instead of out through the reverse proxy and back.
//
// ClientID and NodeID identify *us* to that endpoint. They are labels rather
// than credentials — the internal guard admits every service on the instance
// equally — and their job is to be right in a log line when the revocation cache
// misbehaves, which is why the node name is taken from the OS and never
// invented: a fleet of daemons all calling themselves the same made-up name is
// exactly the diagnostic this field exists to provide.
func newBearerValidator(conf ini.File) (authn.InstanceValidator, error) {
	origin := instconf.InternalOrigin(conf, tokensSection)
	if origin == "" {
		return nil, nil
	}

	node, err := os.Hostname()
	if err != nil {
		return nil, culpa.Wrap(err, "reading the hostname for the tokens.sr.ht node id")
	}

	v, err := bearer.New(bearer.Options{
		Origin:   origin,
		ClientID: serviceName,
		NodeID:   node,
	})
	if err != nil {
		return nil, culpa.Wrapf(err, "assembling the %s plane", tokensSection)
	}
	return v, nil
}

// tokensDescription is what the startup line says about the working-token
// plane, so that "the instance credential is not accepted here" is visible in
// the journal rather than deduced from the first refusal somebody reports.
func tokensDescription(conf ini.File) string {
	origin := instconf.InternalOrigin(conf, tokensSection)
	if origin == "" {
		return "disabled ([" + tokensSection + "] origin is unset)"
	}
	return origin
}