Model access is a provision answered by a record, not a machine
novox/hq ADR 0024, gaps 1 and 2. The user's stated requirement, and the first thing here that no machine can answer: a hosted model is on nobody's node and is reached over the public internet, so the rule that refuses two ends sharing no private network must not apply to it. A licence is a named thing and the name is the operator's — *the personal account*, *the organisation's* — because the whole point is saying which one a given consumer uses, and an anonymous credential hanging off a provider cannot be said. Many to many, so deliberately not a claim: two machines sharing an account is ordinary rather than a collision. Gap 2 is the missing verb, *accept*: take a value somebody supplied, seal it to each holder, discard the plaintext. With the consequence stated rather than hidden — a holder recorded after the key was supplied has no key and the mesh cannot make one, so it is refused by name with the remedy, not silently handed an empty file. Refusal is felt, as the record warns: a mesh holding three ways to reach a model refuses every consumer that has not chosen. So the refusal names the candidates and the exact command. Being right is not the same as being usable. Gaps 3 and 4 — a consumer that is not a machine, and switching as a reaction rather than a declaration — remain gaps. Half-building them would put a conditional in the declaration language, which is what ADR 0024 says plainly to avoid. Its own context, with its own store and its own credential: a licence is a different aggregate from anything inventory owns, and it refers to nodes by name because that is what crossing a context boundary may carry.
This commit is contained in:
@@ -157,6 +157,17 @@ func (r Resolution) Declaration(with Rendering) ([]map[string]any, error) {
|
||||
found = &r.Needs[i]
|
||||
}
|
||||
}
|
||||
if found != nil && found.ByRecord && found.Sealed == "" {
|
||||
// Answered by a record whose key has not been supplied since this consumer was
|
||||
// put on it. **Refused, not skipped.** The mesh discarded the plaintext when the
|
||||
// key was accepted and cannot seal another, so a machine that resolved cleanly
|
||||
// would receive no file at all and fail at whatever tried to read it — which is
|
||||
// the outcome ADR 0024 exists to avoid, arrived at politely.
|
||||
return nil, fmt.Errorf(
|
||||
"%s on this machine uses the licence %q and no key has been sealed to it. "+
|
||||
"The mesh cannot make one; supply it again with `licence key %s`",
|
||||
m.Module, found.From, found.From)
|
||||
}
|
||||
if found == nil || found.Sealed == "" {
|
||||
// Answered on this machine, or answered by a node the mesh could not seal to.
|
||||
// Nothing to write either way, and writing an empty credential file would be
|
||||
@@ -408,11 +419,17 @@ func sortedKeys[V any](m map[string]V) []string {
|
||||
// says so rather than leaving a reader to wonder whether one was meant to be there — a missing
|
||||
// field looks like a bug, and a stated absence looks like a boundary.
|
||||
func boundFile(n Needed, path string) (map[string]any, error) {
|
||||
// A record has no machine and no address. Saying so is the difference between a reader
|
||||
// concluding "somewhere with no address" and concluding the mesh failed to fill something in.
|
||||
where := any(n.At)
|
||||
if n.ByRecord {
|
||||
where = "a record in this mesh, not a machine"
|
||||
}
|
||||
body, err := json.MarshalIndent(map[string]any{
|
||||
"binding": 1,
|
||||
"provision": n.Name,
|
||||
"from": n.From,
|
||||
"at": n.At,
|
||||
"at": where,
|
||||
"serves": n.Serves,
|
||||
"generated": "by the mesh — do not edit; replaced whenever this changes. " +
|
||||
"It carries no credential: the mesh has no way to issue one yet",
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func aModelUser() Manifest {
|
||||
return Manifest{Module: "assistant", Requires: []string{"model-access"},
|
||||
Binds: map[string]string{"model-access": "/etc/assistant/model.json"},
|
||||
Secrets: map[string]string{"model-access": "/etc/assistant/key"}}
|
||||
}
|
||||
|
||||
// A provision answered by a record rather than a node.
|
||||
//
|
||||
// novox/hq ADR 0024: a hosted model is on nobody's machine and is reached over the public
|
||||
// internet, so the rule that refuses two ends sharing no private network must not apply. This
|
||||
// node is deliberately not on the private network at all — under the old rule that alone would
|
||||
// refuse it.
|
||||
func TestAProvisionAnsweredByARecordDoesNotNeedAPrivateNetwork(t *testing.T) {
|
||||
got, err := Resolve(
|
||||
map[string]Manifest{"assistant": aModelUser()},
|
||||
[]string{"assistant"},
|
||||
Node{Name: "workstation"},
|
||||
World{
|
||||
Licences: map[string][]Record{"model-access": {{Name: "personal",
|
||||
Serves: map[string]any{"model": "a-model"}}}},
|
||||
Using: map[string]map[string]Record{"assistant": {"model-access": {Name: "personal",
|
||||
Serves: map[string]any{"model": "a-model"}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("a machine off the private network could not be given model access: %v", err)
|
||||
}
|
||||
if len(got.Needs) != 1 {
|
||||
t.Fatalf("the licence was not recorded as something this node takes: %+v", got.Needs)
|
||||
}
|
||||
if !got.Needs[0].ByRecord {
|
||||
t.Fatal("the licence was treated as a machine, so the reachability rule would apply to it")
|
||||
}
|
||||
if got.Needs[0].From != "personal" {
|
||||
t.Fatalf("the licence is not named by what a person calls it: %+v", got.Needs[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Refused when the consumer has not said which — and the refusal names the candidates and the
|
||||
// command, because ADR 0024 warns this will be felt: a mesh holding three ways to reach a model
|
||||
// refuses every consumer that has not chosen.
|
||||
func TestAConsumerThatHasNotSaidWhichLicenceIsRefusedWithTheCandidates(t *testing.T) {
|
||||
_, err := Resolve(
|
||||
map[string]Manifest{"assistant": aModelUser()},
|
||||
[]string{"assistant"},
|
||||
Node{Name: "workstation"},
|
||||
World{Licences: map[string][]Record{"model-access": {
|
||||
{Name: "personal"}, {Name: "the-organisation"},
|
||||
}}})
|
||||
if err == nil {
|
||||
t.Fatal("a consumer was given model access without anybody saying which")
|
||||
}
|
||||
said := err.Error()
|
||||
for _, want := range []string{"personal", "the-organisation", "licence use"} {
|
||||
if !strings.Contains(said, want) {
|
||||
t.Fatalf("the refusal does not name %q, so it is correct and unusable:\n%s", want, said)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A model the mesh runs itself answers it locally, and a record is not consulted.
|
||||
func TestAModelInTheMeshsOwnSetAnswersItWithoutALicence(t *testing.T) {
|
||||
got, err := Resolve(
|
||||
map[string]Manifest{
|
||||
"assistant": aModelUser(),
|
||||
"ollama": {Module: "ollama",
|
||||
Provides: []Offer{{Name: "model-access", Scope: ScopeNode}}},
|
||||
},
|
||||
[]string{"assistant", "ollama"},
|
||||
Node{Name: "workstation"},
|
||||
World{Licences: map[string][]Record{"model-access": {{Name: "personal"}}}})
|
||||
if err != nil {
|
||||
t.Fatalf("a machine running its own model was asked to choose a licence: %v", err)
|
||||
}
|
||||
for _, n := range got.Needs {
|
||||
if n.ByRecord {
|
||||
t.Fatal("a record was used although the answer was on this machine")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A key that was never supplied is refused by name rather than silently not written.
|
||||
//
|
||||
// The mesh discarded the plaintext when the key was accepted and cannot seal another, so a
|
||||
// machine that resolved cleanly would receive no file and fail at whatever read it.
|
||||
func TestAModuleOnALicenceWithNoKeyIsRefusedRatherThanLeftEmpty(t *testing.T) {
|
||||
r := Resolution{
|
||||
Node: "workstation",
|
||||
Modules: []Manifest{aModelUser()},
|
||||
Needs: []Needed{{Name: "model-access", From: "personal", ByRecord: true, For: "assistant"}},
|
||||
}
|
||||
_, err := r.Declaration(Rendering{})
|
||||
if err == nil {
|
||||
t.Fatal("a module was given a licence with no key, so it receives nothing and fails later")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "licence key personal") {
|
||||
t.Fatalf("the refusal does not say how to fix it: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// And with a key, both files arrive: what is public, and what is not.
|
||||
func TestALicenceDeliversWhatIsPublicAndWhatIsSealed(t *testing.T) {
|
||||
r := Resolution{
|
||||
Node: "workstation",
|
||||
Modules: []Manifest{aModelUser()},
|
||||
Needs: []Needed{{Name: "model-access", From: "personal", ByRecord: true, For: "assistant",
|
||||
Serves: map[string]any{"model": "a-model"}, Sealed: "sealed-blob"}},
|
||||
}
|
||||
out, err := r.Declaration(Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files := map[string]map[string]any{}
|
||||
for _, res := range out {
|
||||
if path, ok := res["path"].(string); ok {
|
||||
files[path] = res
|
||||
}
|
||||
}
|
||||
bound, given := files["/etc/assistant/model.json"]
|
||||
if !given {
|
||||
t.Fatal("the consumer was not told what it needs to know that is not secret")
|
||||
}
|
||||
content, _ := bound["content"].(string)
|
||||
if !strings.Contains(content, "a-model") {
|
||||
t.Fatalf("the binding does not carry what the licence serves:\n%s", content)
|
||||
}
|
||||
// The binding says it is a record rather than leaving an empty address, which a reader would
|
||||
// take for something the mesh failed to fill in.
|
||||
if !strings.Contains(content, "not a machine") {
|
||||
t.Fatalf("the binding leaves an empty address with no explanation:\n%s", content)
|
||||
}
|
||||
key, delivered := files["/etc/assistant/key"]
|
||||
if !delivered {
|
||||
t.Fatal("the key was not delivered")
|
||||
}
|
||||
if key["sealed"] != "sealed-blob" {
|
||||
t.Fatalf("the key is not the sealed one: %+v", key)
|
||||
}
|
||||
// And never in the open. The whole arrangement is that what travels is unreadable by
|
||||
// everything between here and the machine.
|
||||
if strings.Contains(content, "sealed-blob") {
|
||||
t.Fatal("the key was written into the public file as well")
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,18 @@ type World struct {
|
||||
// start meaning something the day a second provider appears, and one recorded and then made
|
||||
// unnecessary should not quietly stop applying either.
|
||||
Pinned map[string]string
|
||||
// Licences is every provision answered by a **record rather than a node**, by provision name.
|
||||
//
|
||||
// novox/hq ADR 0024: a hosted model is on nobody's machine and is reached over the public
|
||||
// internet, so the rule that refuses two ends sharing no private network must not apply to
|
||||
// it. These are the candidates a refusal names.
|
||||
Licences map[string][]Record
|
||||
// Using is which record this node's modules were put on, keyed by module then provision.
|
||||
//
|
||||
// Per consumer, because that is the whole point: saying WHICH licence a given thing uses. Two
|
||||
// modules on one machine using different accounts is ordinary rather than a collision.
|
||||
Using map[string]map[string]Record
|
||||
|
||||
// Unchecked takes brokered requirements on trust instead of refusing when nothing answers
|
||||
// them.
|
||||
//
|
||||
@@ -50,6 +62,18 @@ type World struct {
|
||||
Unchecked bool
|
||||
}
|
||||
|
||||
// Record is a provision answered by something the mesh holds rather than by a machine.
|
||||
//
|
||||
// The name is the operator's — *the personal account*, *the organisation's* — because the whole
|
||||
// point is saying which one a consumer uses, and an anonymous credential hanging off a provider
|
||||
// cannot be said (novox/hq ADR 0024).
|
||||
type Record struct {
|
||||
// Name is what a person calls it, and what a consumer is put on.
|
||||
Name string
|
||||
// Serves is what a consumer must know that is not secret — a base URL, a model name.
|
||||
Serves map[string]any
|
||||
}
|
||||
|
||||
// Provider is one node answering a mesh-scoped requirement.
|
||||
type Provider struct {
|
||||
// Node is the machine.
|
||||
@@ -101,6 +125,10 @@ type Needed struct {
|
||||
At string
|
||||
// Serves is what the providing module said a consumer needs to know.
|
||||
Serves map[string]any
|
||||
// ByRecord means this was answered by something the mesh holds rather than by a machine, so
|
||||
// there is no node to reach and no private network to share. Its credential comes from
|
||||
// wherever that record's does, not from the pair-wise secret two machines share.
|
||||
ByRecord bool
|
||||
// Sealed is the credential, closed to this node. Filled in after resolving, because whose
|
||||
// credential it is only becomes answerable once which node answers has been settled.
|
||||
Sealed string
|
||||
@@ -288,6 +316,14 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
||||
candidates := offers[want]
|
||||
switch len(candidates) {
|
||||
case 0:
|
||||
if len(world.Licences[want]) > 0 {
|
||||
// Answered by a record rather than by a module, and the post-pass below settles
|
||||
// which one. Left alone here: the two questions a refusal must answer — *is
|
||||
// there anything* and *which one* — have different remedies, and answering the
|
||||
// first wrongly would send somebody looking for a module to install.
|
||||
reported[want] = true
|
||||
continue
|
||||
}
|
||||
reported[want] = true
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"nothing provides %q, wanted by %s", want, because[want]))
|
||||
@@ -324,6 +360,53 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
||||
}
|
||||
}
|
||||
|
||||
// What is answered by a record rather than by a machine.
|
||||
//
|
||||
// A post-pass, deliberately: nothing about it depends on the order requirements were walked
|
||||
// in, and putting it in the queue would mean the reachability rule — which must not apply
|
||||
// here — sitting one branch away from a case it would be wrong for.
|
||||
//
|
||||
// **Refused when the consumer has not said which.** ADR 0024 warns this will be felt: a mesh
|
||||
// holding three ways to reach a model refuses every consumer that has not chosen, which is
|
||||
// correct and is a great deal of saying-which the first time. So the refusal names the
|
||||
// candidates and the exact command, because being right is not the same as being usable.
|
||||
for _, name := range order {
|
||||
m := catalogue[name]
|
||||
for _, want := range m.Wants() {
|
||||
offered, byRecord := world.Licences[want]
|
||||
if !byRecord || len(offered) == 0 {
|
||||
continue
|
||||
}
|
||||
if satisfied[want] {
|
||||
// Something in this node's own set answers it -- a model the mesh runs itself,
|
||||
// most obviously. A record is not consulted when there is a local answer.
|
||||
continue
|
||||
}
|
||||
using, said := world.Using[m.Module][want]
|
||||
if !said {
|
||||
if world.Unchecked {
|
||||
continue
|
||||
}
|
||||
names := make([]string, 0, len(offered))
|
||||
for _, r := range offered {
|
||||
names = append(names, r.Name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s on %s needs %q and has not been told which one to use — say which with "+
|
||||
"`licence use <name> %s %s`: %s",
|
||||
m.Module, node.Name, want, node.Name, m.Module, strings.Join(names, ", ")))
|
||||
continue
|
||||
}
|
||||
needs = append(needs, Needed{
|
||||
Name: want, From: using.Name, Serves: using.Serves, ByRecord: true,
|
||||
// The module that required it, not whatever first mentioned the name: the key is
|
||||
// sealed per consumer, and a consumer here is a module on a machine.
|
||||
For: m.Module,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resolution := Resolution{Node: node.Name, At: node.At, Because: because, Needs: needs}
|
||||
for _, n := range order {
|
||||
resolution.Modules = append(resolution.Modules, catalogue[n])
|
||||
|
||||
Reference in New Issue
Block a user