Files
mesh-tools/node-tools/internal/provide/provide.go
T
mesh-tools 33d136e702
mesh/merge-gate head 33d136e70227 merged into trunk 6b3e15042fee — pass, with a note: builds mesh-tools, node-tools (+40 dependent(s)) → ace, g14, novox, s…
mesh/repo-check head 33d136e70227 merged into trunk 6b3e15042fee — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the ch…
mesh/delivery delivered
Feature 57 (config and provisioning): release mesh-tools (the provider's StateBus, the SDK pin, templates), reviewed
2026-10-16 09:41:17 +00:00

133 lines
4.5 KiB
Go

// Package provide is the runtime's half of node-tools being a provider of `mcp-endpoint`: it runs the mesh SDK's
// provider helper, so a consumer handed to the provisioner (claude-code's `${bound:mcp-endpoint:port}`) is answered
// from an entry node-tools writes in its own bucket, not from the controller's binding file (novox/hq ADR 0349
// clause 12, issue 791).
//
// The offer carries no credential: the console's authority is the machine's login (ADR 0152, 0175), so the helper
// reads nothing from the vault, needs no consumers' file and the entry names none. node-tools is not marked moved:
// its manifest says `"answered-by": "helper"` for the provision, and the helper runs whenever the runtime does
// (issue 791), as route-proxy's does (issue 792).
package provide
import (
"context"
"encoding/json"
"fmt"
"time"
"git.novox.be/novox/mesh-sdk/go/provider"
"github.com/novox/mesh-tools/node-tools/internal/bus"
)
const (
// Module is the provider: the runtime's own module.
Module = "node-tools"
// Provision is what node-tools provides, as its manifest says.
Provision = "mcp-endpoint"
// ServedPort is the port its manifest serves (`serves.mcp-endpoint.port`): where the console listens.
ServedPort = 4270
)
// Offers is what node-tools serves, per provision.
func Offers() map[string]provider.Offer {
return map[string]provider.Offer{Provision: {
Credential: provider.NoCredential,
Serves: map[string]any{"port": ServedPort},
}}
}
// busOf is the helper's bus over the runtime's own state: the provider's bucket is its state `bound`, and the
// controller's assignments are the state it reads, `mesh-controller.assignments` (both declared in the manifest).
type busOf struct{ conn *bus.Conn }
func (b busOf) state(bucket string) (string, error) {
switch bucket {
case provider.BucketOf(Module):
return "bound", nil
case provider.AssignmentsBucket:
return "mesh-controller.assignments", nil
}
return "", fmt.Errorf("%s is not a bucket this provider keeps or reads", bucket)
}
func (b busOf) Watch(ctx context.Context, bucket string, handle func(key string, value []byte)) error {
name, err := b.state(bucket)
if err != nil {
return err
}
stop, err := b.conn.StateWatch(Module, name, "", func(ch bus.StateChange) error {
if ch.Op == "delete" {
handle(ch.Key, nil)
} else {
handle(ch.Key, ch.Value)
}
return nil
})
if err != nil {
return err
}
go func() { <-ctx.Done(); stop() }()
return nil
}
func (b busOf) Put(_ context.Context, bucket, key string, value []byte) error {
name, err := b.state(bucket)
if err != nil {
return err
}
_, err = b.conn.StatePut(Module, name, key, json.RawMessage(value))
return err
}
func (b busOf) Delete(_ context.Context, bucket, key string) error {
name, err := b.state(bucket)
if err != nil {
return err
}
return b.conn.StateDelete(Module, name, key)
}
// New is the helper answering node-tools's consumers on node.
func New(conn *bus.Conn, node string, every time.Duration, logf func(string, ...any)) *provider.Provider {
return provider.New(provider.Options{
Node: node,
Module: Module,
Offers: Offers(),
Bus: busOf{conn: conn},
Every: every,
Logf: logf,
})
}
// Run answers the consumers until ctx ends. The state is not reachable until the mesh has issued the runtime its
// membership and made the buckets, so a start that cannot watch is tried again, not given up.
func Run(ctx context.Context, conn *bus.Conn, node string, every time.Duration, logf func(string, ...any)) {
for delay := 2 * time.Second; ctx.Err() == nil; delay = min(delay*2, time.Minute) {
attempt, cancel := context.WithCancel(ctx)
err := New(conn, node, every, logf).Run(attempt)
cancel()
if ctx.Err() != nil {
return
}
logf("[mesh-tools] the provider helper stopped: %v; trying again in %s", err, delay)
select {
case <-ctx.Done():
case <-time.After(delay):
}
}
}
// Start runs the helper when this process is the machine's runtime (the node-tools credential, on a machine's
// name), with no condition on the environment, and returns what ends it. A runtime on another credential, a module's
// own in a test, is not the provider and answers nobody.
func Start(conn *bus.Conn, module, node string, every time.Duration, logf func(string, ...any)) (stop func()) {
if module != Module || node == "" {
return func() {}
}
ctx, cancel := context.WithCancel(context.Background())
logf("[mesh-tools] answering %s's consumers of %s through the provider helper", Module, Provision)
go Run(ctx, conn, node, every, logf)
return cancel
}