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
133 lines
4.5 KiB
Go
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
|
|
}
|