From 38a0bbca44e36b2ff721c42629b39ba6dd4372f9 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 00:57:45 +0100 Subject: [PATCH] Let enrolment-token take the first tunnel, so the token the vault makes carries it as today MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lead's decision on milestone point 7 (a progressive insight of ADR 0587 §3): mesh-enrolment builds the joining node's first tunnel at issue and passes it to the vault's enrolment-token as `tunnel`, an object of hub_key, hub_endpoint and address, each required; the vault puts it in the token (delivery.TokenTunnel). Tested as a row of TestTheVaultsSeatPromisesItsIntakeVerbs; no new test function (1438 unit tests). (issue 774, review round 1) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- go/catalogue/seats.go | 11 ++++++++--- go/catalogue/vault_seat_test.go | 15 +++++++++++++-- go/catalogue/verbs.go | 10 ++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index e0da9b7..6de2c61 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -249,11 +249,16 @@ var defaultSeats = append([]Seat{ "written as the bus user enrol.. Answers only what the joining machine must trust (the bus's " + "address, the bus certificate's fingerprint, the vault's public signing key), never the token; with " + "sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash " + - "dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write.", - Input: schema(map[string]string{"node": "the node the token is for", + "dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token " + + "carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", + Input: withObject(schema(map[string]string{"node": "the node the token is for", "expires": "when the token expires (RFC 3339), as the enrolment's key says", "sealed": "\"true\": answer the token sealed to the operator key", - "drop": "\"true\": drop the node's token and its hash"}, []string{"node"}, "sealed", "drop")}, + "drop": "\"true\": drop the node's token and its hash"}, []string{"node"}, "sealed", "drop"), + "tunnel", "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's "+ + "public key, the hub's endpoint and the node's tunnel address", + map[string]string{"hub_key": "the hub's tunnel public key", "hub_endpoint": "the hub's endpoint, host:port", + "address": "the joining node's tunnel address"})}, {Name: "node-account", Optional: true, Description: "Mint a joining node's bus account node., sealed " + "to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the " + "account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the " + diff --git a/go/catalogue/vault_seat_test.go b/go/catalogue/vault_seat_test.go index 28cd326..b960619 100644 --- a/go/catalogue/vault_seat_test.go +++ b/go/catalogue/vault_seat_test.go @@ -1,6 +1,9 @@ package catalogue -import "testing" +import ( + "slices" + "testing" +) // The vault's provision is one the controller itself dereferences — every minted credential is // sealed with it — so it is delivered by a seat of the mesh's own, and a second provider is a second @@ -66,7 +69,7 @@ func TestTheVaultsSeatPromisesItsIntakeVerbs(t *testing.T) { "issued": {"provision", "provider", "module", "call", "sealed", "holders"}, "rotate": {"node", "module", "name", "provider", "provision", "by"}, // 774/S37/AC1: the enrolment verbs mesh-enrolment asks (ADR 0587 §3, §5). - "enrolment-token": {"node", "expires", "sealed", "drop"}, + "enrolment-token": {"node", "expires", "sealed", "drop", "tunnel"}, "node-account": {"node", "sealing_key", "keys_digest", "retire"}, "sign": {"kind", "node", "serving_key", "reply"}, // 774/S38: the rotation verbs (ADR 0583 §2, §3, §5). @@ -97,6 +100,14 @@ func TestTheVaultsSeatPromisesItsIntakeVerbs(t *testing.T) { if props["value"] != nil { t.Errorf("%s takes a value in the clear", name) } + // The first tunnel the token carries (ADR 0587 §3, insight of 2026-11-02): an object of the hub's key, its endpoint + // and the node's address. + if name == "enrolment-token" { + tunnel, _ := props["tunnel"].(map[string]any) + if tunnel["type"] != "object" || !slices.Equal(tunnel["required"].([]string), []string{"address", "hub_endpoint", "hub_key"}) { + t.Errorf("enrolment-token's tunnel is %v", tunnel) + } + } } // The holders `issued` names (issue 749) are a list of {node, module}. for _, verb := range seat.Serves { diff --git a/go/catalogue/verbs.go b/go/catalogue/verbs.go index 9f980fb..9c1bdac 100644 --- a/go/catalogue/verbs.go +++ b/go/catalogue/verbs.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/json" "fmt" + "maps" + "slices" ) // A Verb is one tool a role serves: its name, what it does, and the schema of its arguments and of @@ -596,6 +598,14 @@ var ControllerVerbs = []Verb{ // // **The schema is the whole of what a verb takes** (novox/hq issue 244): an argument it does not // name is refused when the verb is called, never passed over. +// withObject adds to a verb's input s the property name, an object of the string fields given, each required. +func withObject(s map[string]any, name, description string, fields map[string]string) map[string]any { + inner := schema(fields, slices.Sorted(maps.Keys(fields))) + inner["description"], inner["additionalProperties"] = description, false + s["properties"].(map[string]any)[name] = inner + return s +} + func schema(properties map[string]string, required []string, switches ...string) map[string]any { isSwitch := map[string]bool{} for _, s := range switches {