Replace a value given by hand like one the mesh made (hq ADR 0228)

A given own secret the module reads at start is held by nobody but that
module, so the mesh need not read it to replace it: secret rotate now
works on it, and a value given through secret accept is replaced on its
own after the module's first good start under the mesh. Only a value an
outside party issues (own-secrets "issued-by": "outside") or one the
module applies stays as given, refused with the reason.
This commit is contained in:
jochen
2026-10-06 12:13:48 +02:00
parent 722682f1c4
commit e51c6a2cb9
20 changed files with 761 additions and 60 deletions
+1 -1
View File
@@ -168,7 +168,7 @@ func placedManifest(m Manifest, with Rendering) (Manifest, error) {
if err != nil {
return m, err
}
own[name] = OwnSecret{Path: filled, Taken: s.Taken}
own[name] = OwnSecret{Path: filled, Taken: s.Taken, IssuedBy: s.IssuedBy}
}
m.OwnSecrets = own
}
+42 -10
View File
@@ -558,7 +558,10 @@ type Manifest struct {
// that takes it only once, so a rotation must be staged beside the current value — the form the
// mesh does not build yet, and refuses by name. A secret that says neither is not rotated by
// the mesh: the one fault worse than an unrotated credential is a rotated one the software
// never saw.
// never saw. `"issued-by": "outside"` says a party outside the mesh issues the value — an API
// key, a bot token, a licence — so the mesh never puts one of its own in its place (novox/hq
// ADR 0228); any other at-start secret given by hand lives only until the module's first good
// start, and is then replaced.
OwnSecrets OwnSecrets `json:"own-secrets,omitempty"`
// SecretsOwner is who the files holding this module's secrets belong to on the machine —
@@ -1801,6 +1804,13 @@ func ParseManifest(raw []byte) (Manifest, error) {
"or %q (applied by the module's own code to a backend that takes it once)",
m.Module, name, own.Taken, TakenAtStart, TakenApplied))
}
if own.IssuedBy != "" && own.IssuedBy != IssuedOutside {
problems = append(problems, fmt.Sprintf(
"%s says its secret %q is issued by %q; a secret says %q when a party outside the mesh "+
"issues it — a vendor's key, a bot's token, a licence — and says nothing when the mesh "+
"may make it (novox/hq ADR 0228)",
m.Module, name, own.IssuedBy, IssuedOutside))
}
}
localOf := map[string]string{}
for _, to := range m.SecretRequirements() {
@@ -2263,10 +2273,24 @@ const (
TakenApplied = "applied"
)
// OwnSecret is where one of a module's own secrets lands, and how the module takes it.
// IssuedOutside says a value was issued by a party outside the mesh — a vendor's API key, a bot's
// token, a licence — so no value the mesh makes would work in its place (novox/hq ADR 0228).
const IssuedOutside = "outside"
// OwnSecret is where one of a module's own secrets lands, how the module takes it, and — for a
// value only an outside party can issue — that it is one.
type OwnSecret struct {
Path string
Taken string
Path string
Taken string
IssuedBy string
}
// MeshMayMake says the mesh may put a value it makes in place of the one the secret holds: the
// module reads it as it starts, and nobody outside the mesh issued it (novox/hq ADR 0228). A value
// given to the mesh for such a secret lives only until the module's first good start under the
// mesh; anything else given stays as given.
func (s OwnSecret) MeshMayMake() bool {
return s.Taken == TakenAtStart && s.IssuedBy != IssuedOutside
}
// OwnSecrets is a module's own secrets by name. On the wire each is a path, or an object naming
@@ -2287,15 +2311,16 @@ func (o *OwnSecrets) UnmarshalJSON(raw []byte) error {
continue
}
var long struct {
Path string `json:"path"`
Taken string `json:"taken,omitempty"`
Path string `json:"path"`
Taken string `json:"taken,omitempty"`
IssuedBy string `json:"issued-by,omitempty"`
}
dec := json.NewDecoder(bytes.NewReader(body))
dec.DisallowUnknownFields()
if err := dec.Decode(&long); err != nil {
return fmt.Errorf("own-secrets.%s: a path, or {\"path\", \"taken\"}: %w", name, err)
return fmt.Errorf("own-secrets.%s: a path, or {\"path\", \"taken\", \"issued-by\"}: %w", name, err)
}
out[name] = OwnSecret{Path: long.Path, Taken: long.Taken}
out[name] = OwnSecret{Path: long.Path, Taken: long.Taken, IssuedBy: long.IssuedBy}
}
*o = out
return nil
@@ -2304,11 +2329,18 @@ func (o *OwnSecrets) UnmarshalJSON(raw []byte) error {
func (o OwnSecrets) MarshalJSON() ([]byte, error) {
entries := make(map[string]any, len(o))
for name, s := range o {
if s.Taken == "" {
if s.Taken == "" && s.IssuedBy == "" {
entries[name] = s.Path
continue
}
entries[name] = map[string]string{"path": s.Path, "taken": s.Taken}
long := map[string]string{"path": s.Path}
if s.Taken != "" {
long["taken"] = s.Taken
}
if s.IssuedBy != "" {
long["issued-by"] = s.IssuedBy
}
entries[name] = long
}
return json.Marshal(entries)
}
@@ -52,3 +52,42 @@ func TestAnOwnSecretTakenSomeOtherWayIsRefused(t *testing.T) {
t.Fatal("an unknown field on an own secret was accepted")
}
}
// A secret an outside party issues says so (novox/hq ADR 0228), is written back as it was read, and
// is the one at-start secret the mesh may not make; any other word for who issued it is refused.
func TestAnOwnSecretSaysWhenAnOutsidePartyIssuesIt(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"letta","version":"1","own-secrets":{
"server-password":{"path":"/var/lib/letta/server-password","taken":"at-start"},
"openai-api-key":{"path":"/var/lib/letta/openai-api-key","taken":"at-start","issued-by":"outside"},
"telegram-token":{"path":"/var/lib/letta/telegram-token","issued-by":"outside"},
"logflare":{"path":"/var/lib/letta/logflare","taken":"applied"},
"broker":"/var/lib/mesh/letta/broker"}}`))
if err != nil {
t.Fatal(err)
}
for name, may := range map[string]bool{"server-password": true, "openai-api-key": false,
"telegram-token": false, "logflare": false, "broker": false} {
if got := m.OwnSecrets[name].MeshMayMake(); got != may {
t.Errorf("%s: the mesh may make it = %v, want %v", name, got, may)
}
}
out, err := json.Marshal(m.OwnSecrets)
if err != nil {
t.Fatal(err)
}
var again OwnSecrets
if err := json.Unmarshal(out, &again); err != nil {
t.Fatal(err)
}
if again["openai-api-key"] != m.OwnSecrets["openai-api-key"] || again["telegram-token"] != m.OwnSecrets["telegram-token"] {
t.Fatalf("the round trip lost who issued it: %s", out)
}
if strings.Contains(string(out), `"taken":""`) {
t.Fatalf("a secret that says only who issued it gained an empty taken: %s", out)
}
_, err = ParseManifest([]byte(`{"module":"letta","version":"1","own-secrets":{
"openai-api-key":{"path":"/var/lib/letta/openai-api-key","issued-by":"openai"}}}`))
if err == nil || !strings.Contains(err.Error(), `issued by "openai"`) {
t.Fatalf("an unknown word for who issued a secret was accepted: %v", err)
}
}
+3 -1
View File
@@ -85,7 +85,9 @@ var defaultSeats = append([]Seat{
{Name: ControllerSeatName, Scope: ScopeMesh, Decision: "novox/hq ADR 0079",
// And what is wrong, as it changes, and the self-check's heartbeat (novox/hq to-be 45 §2, §4).
Emits: []string{"applied", "refused", "built-before",
"condition-raised", "condition-changed", "condition-cleared", "doctor-heartbeat"},
"condition-raised", "condition-changed", "condition-cleared", "doctor-heartbeat",
// A value given by hand, replaced after its module's first good start (novox/hq ADR 0228).
"secret-replaced"},
Serves: ControllerVerbs},
// The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable,
// served by whichever module holds the seat with tools of these names.
+5 -2
View File
@@ -149,14 +149,17 @@ var ControllerVerbs = []Verb{
}, []string{"why"}, "behind")},
{Name: "rotate", Description: "Replace a credential. A pair credential, by provision (and a consuming machine and module, " +
"else every holder): both ends are re-sent together. Or a module's own secret, by machine, module and " +
"name: made anew and the machine sent, so the module starts again on it — only for a secret its " +
"definition says it reads at start; a value given to the mesh, or one the module applies to a backend, is refused with the reason.",
"name: made anew and the machine sent, so the module starts again on it — for a secret its definition " +
"says it reads at start, whether the mesh made the value or a person gave it (novox/hq ADR 0228); one " +
"an outside party issued, or one the module applies to a backend, is refused with the reason.",
Input: schema(map[string]string{
"provision": "a pair credential: the provision whose credential to replace",
"consumer": "with provision: only the holder on this machine (optional)",
"node": "an own secret: the machine",
"module": "an own secret: the module; with provision: only this consuming module's credential (optional)",
"secret": "an own secret: its name in the module's definition",
"why": "an own secret: why it is rotated — recorded in the hand-act log (optional)",
"cause": "with why: the cause in a word, the word a second rotation for the same reason uses (optional)",
}, nil)},
{Name: "issue", Description: "Give a module on a machine its account on the bus: minted, and sealed to the " +
"machine as the module's own secret named broker, read at the next push of that machine. For a module " +