Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)

This commit is contained in:
2026-10-16 07:37:43 +00:00
parent 1a2c917abf
commit 9652b2d5ce
771 changed files with 52063 additions and 1774 deletions
+2 -2
View File
@@ -48,7 +48,7 @@ func tools() []stdio.Tool {
Description: fmt.Sprintf("Ask the operator for a value in a prompt that shows nothing of what is typed, "+
"and answer it sealed to the key seal_to names — never in the clear (novox/hq ADR 0259 §10): how the "+
"mesh takes a module's own secret at the desk. The prompt is written here from the names, and says the "+
"mesh's controller asks: the bus lets nobody else ask it. Answers sealed, or cancelled when the prompt was "+
"mesh's controller or vault asks: the bus lets nobody else ask it. Answers sealed, or cancelled when the prompt was "+
"dismissed, answered empty or not answered within timeout_seconds (default and at most %d).", menuMost),
Input: map[string]any{
"type": "object",
@@ -62,7 +62,7 @@ func tools() []stdio.Tool {
"required": []string{"module", "secret", "node", "seal_to"},
},
Run: func(args map[string]any) (any, error) {
q, err := secretQuestion(args)
q, err := secretQuestion(stdio.CallerOf(args), args)
if err != nil {
return nil, err
}
@@ -13,6 +13,7 @@ import (
type manifest struct {
Module string `json:"module"`
Moved bool `json:"moved"`
Version string `json:"version"`
Capabilities []string `json:"capabilities"`
Requires []string `json:"requires"`
@@ -23,7 +24,8 @@ type manifest struct {
Shell []shellCode `json:"shell"`
Resources []map[string]any `json:"resources"`
// Lines for other modules' seats (novox/hq ADR 0212): the window manager's, here.
Contributions []contribution `json:"contributions"`
Items []item `json:"items"`
Contributions []any `json:"contributions"`
Build struct {
Artifacts []map[string]any `json:"artifacts"`
} `json:"build"`
@@ -85,26 +87,66 @@ func (m manifest) packages() (present, absent []string) {
return present, absent
}
// sameAsSource checks that a file resource's content is byte for byte the module's source file, so
// the readable file in the repository is what the machine gets.
func (m manifest) sameAsSource(t *testing.T, id, source string) {
// fromTemplate checks that a file resource takes its text from its template file in the module's
// repository (novox/hq ADR 0349 §2): it names the template, carries no text of its own beside it, and the
// template is there. The readable file in the repository is the one place the machine's text is written.
func (m manifest) fromTemplate(t *testing.T, id, template string) {
t.Helper()
want, err := os.ReadFile(filepath.Join("..", "..", source))
if err != nil {
t.Fatal(err)
}
r := m.resource(t, id)
if r["type"] != "file" {
t.Fatalf("%s is a %v, not a file", id, r["type"])
}
if got, _ := r["content"].(string); got != string(want) {
t.Fatalf("resource %s's content is not %s: edit the source and copy it into module.json", id, source)
if r["template"] != template {
t.Fatalf("resource %s does not take its text from %s: template %v", id, template, r["template"])
}
if _, inline := r["content"]; inline {
t.Fatalf("resource %s keeps its text inline beside its template: a file's text is in one place", id)
}
if _, err := os.Stat(filepath.Join("..", "..", template)); err != nil {
t.Fatalf("resource %s names %s: %v", id, template, err)
}
if r["owner"] != "${machine:account}" && !strings.HasPrefix(r["path"].(string), "/etc/") {
t.Fatalf("%s under the home is the account's", id)
}
}
// everyFileIsATemplate checks that no file resource carries its text inline, and that the build packs
// files/ into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
func (m manifest) everyFileIsATemplate(t *testing.T) {
t.Helper()
for _, r := range m.Resources {
if _, inline := r["content"]; inline {
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
}
}
packs := 0
for _, a := range m.Build.Artifacts {
if a["kind"] == "archive" && a["from"] == "files" {
packs++
if a["name"] != "files" {
t.Errorf("the templates archive is named %v, not files", a["name"])
}
}
}
if packs != 1 {
t.Errorf("the build packs files/ into %d archives, not one", packs)
}
}
// text is the text a file resource places on the machine: its template file's.
func (m manifest) text(t *testing.T, id string) string {
t.Helper()
name, _ := m.resource(t, id)["template"].(string)
if name == "" {
t.Fatalf("resource %s names no template", id)
}
raw, err := os.ReadFile(filepath.Join("..", "..", name))
if err != nil {
t.Fatal(err)
}
return string(raw)
}
// checkTheToolsAgree checks that the manifest lists the module's own tools exactly, that the bundle
// serves each seat verb the claims promise as <seat>.<verb>, and that the Go bundle is declared.
func checkTheToolsAgree(t *testing.T, m manifest) {
@@ -176,31 +218,49 @@ func checkNoSecretsOrInstallationNames(t *testing.T) {
}
}
type contribution struct {
Seat string `json:"seat"`
Kind string `json:"kind"`
Content string `json:"content"`
}
// displaySession is the collection the module's lines for the window manager are an item of.
const displaySession = "contribution:node-display-session:config"
// i3Lines is what the module contributes to the window manager.
// i3Lines is what the module contributes to the window manager: the text of its item of the collection
// contribution:node-display-session:config (design 57 L3, issue 848).
func (m manifest) i3Lines() string {
var out string
for _, c := range m.Contributions {
if c.Seat == "node-display-session" && c.Kind == "config" {
out += c.Content
for _, it := range m.Items {
if it.Collection == displaySession {
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
if err != nil {
return ""
}
out += string(text)
}
}
return out
}
// i3LinesAreSource checks the window-manager contribution is the source file it is written from.
// i3LinesAreSource checks the window-manager lines are one item whose template is the source file, and that no
// old-form contribution to node-display-session is left.
func (m manifest) i3LinesAreSource(t *testing.T, source string) {
t.Helper()
want, err := os.ReadFile(filepath.Join("..", "..", source))
if err != nil {
t.Fatal(err)
if len(m.Contributions) != 0 {
t.Fatalf("the module still holds contributions: the lines are an item of %s", displaySession)
}
if m.i3Lines() != string(want) {
t.Fatalf("the contribution to node-display-session is not %s: edit the source and copy it into module.json", source)
var templates []string
for _, it := range m.Items {
if it.Collection == displaySession {
templates = append(templates, it.Template)
}
}
if len(templates) != 1 || templates[0] != source {
t.Fatalf("the items of %s are %v, want the one template %s", displaySession, templates, source)
}
if m.i3Lines() == "" {
t.Fatalf("the template %s holds no line", source)
}
}
type item struct {
Collection string `json:"collection"`
Name string `json:"name"`
Attributes map[string]string `json:"attributes"`
Template string `json:"template"`
}
+10 -5
View File
@@ -37,7 +37,7 @@ func TestItOwnsItsFilesAsWrittenInTheModule(t *testing.T) {
"launch": "files/bin/rofi-launch",
"powermenu": "files/bin/rofi-powermenu",
} {
m.sameAsSource(t, id, source)
m.fromTemplate(t, id, source)
}
for _, id := range []string{"dmenu", "launch", "powermenu"} {
if m.resource(t, id)["mode"] != "0755" {
@@ -51,25 +51,25 @@ func TestItOwnsItsFilesAsWrittenInTheModule(t *testing.T) {
func TestTheThemesAreTheMeshsFacesAndTheConfigurationNamesOne(t *testing.T) {
m := readManifest(t)
conf := m.resource(t, "configuration")["content"].(string)
conf := m.text(t, "configuration")
if !strings.Contains(conf, `@theme "mesh"`) || strings.Contains(conf, "@import") {
t.Fatal("the configuration names the theme mesh and imports nothing")
}
for _, id := range []string{"theme", "theme-powermenu"} {
c := m.resource(t, id)["content"].(string)
c := m.text(t, id)
if !strings.Contains(c, `"JetBrainsMono Nerd Font`) || strings.Contains(c, "@import") ||
strings.Contains(c, "Hack") || strings.Contains(c, "Iosevka") {
t.Errorf("%s: the monospace face, and no imported file", id)
}
}
if !strings.Contains(m.resource(t, "theme")["content"].(string), `"Inter 11"`) {
if !strings.Contains(m.text(t, "theme"), `"Inter 11"`) {
t.Error("the theme's prose is in the interface face")
}
}
func TestThePowerMenuNamesNoWindowManagerAndNoLocker(t *testing.T) {
m := readManifest(t)
menu := m.resource(t, "powermenu")["content"].(string)
menu := m.text(t, "powermenu")
for _, never := range []string{"i3-msg", "i3lock", "betterlockscreen", "openbox", "bspc", "qdbus", "mpc"} {
if strings.Contains(menu, never) {
t.Errorf("the power menu names %s", never)
@@ -109,3 +109,8 @@ func TestTheToolsAgreeWithTheManifest(t *testing.T) {
checkTheToolsAgree(t, m)
checkNoSecretsOrInstallationNames(t)
}
// No file resource carries its text inline (novox/hq ADR 0349 §2).
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
readManifest(t).everyFileIsATemplate(t)
}
+52 -13
View File
@@ -14,12 +14,13 @@ import (
// node-launcher's verb `secret` (novox/hq ADR 0259 §10): a prompt that does not show what is typed, whose
// answer is sealed to the key the asker gives before it leaves this process — never in the clear on the
// bus, in the runtime's log or in any call's record. The controller asks it to take a module's own secret,
// such as a bot token, from the operator at their desk.
// bus, in the runtime's log or in any call's record. The vault asks it, in its own name, to take a module's own
// secret, such as a bot token, from the operator at their desk; the controller asks it for a module still filled
// the old way, until R1 (issue 682).
//
// **What it does not do**: on an X11 desk any program of the operator's account can read the keys as they
// are typed, and a program that asks this verb with a key of its own is answered with what the operator
// typed into a prompt they did not ask for. The bus lets only the controller ask this verb, and the prompt,
// typed into a prompt they did not ask for. The bus lets only the controller and the vault ask this verb, and the prompt,
// written here from names, says so and for what, so the operator types only into one they started with give
// (ADR 0234's accepted residual risk is what remains: a program drawing a window of its own).
@@ -46,11 +47,22 @@ var aName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
// break, no markup, no punctuation a sentence is written with — so the prompt's words stay this holder's.
var aCaller = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/@ -]{0,79}$`)
// theVault says the caller the runtime proved is the vault's own account (`<node>.mesh-vault`, issue 723): the one
// account beside the controller the bus lets ask this verb, and the only one that asks it in its own name.
func theVault(caller string) bool { return strings.HasSuffix(caller, ".mesh-vault") }
// escapeMarkup is `what` shown as text, never markup (novox/hq issue 789): the window's message is read with
// pango markup on, so a catalogue's words reach it with the characters pango gives meaning to turned to their
// entities first — never interpreted, whatever a module's manifest holds.
var escapeMarkupReplacer = strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
func escapeMarkup(s string) string { return escapeMarkupReplacer.Replace(s) }
// secretQuestion reads the call. The prompt is this holder's words, written from the module, the secret and the
// machine (the review of 2026-10-09, M4): it says the mesh's controller asks, which the bus makes true — it
// denies these subjects to every principal but the controller — and a caller's own words are refused, so
// nobody puts a prompt of its choosing before the operator.
func secretQuestion(args map[string]any) (SecretQuestion, error) {
// machine (the review of 2026-10-09, M4): it says the mesh's controller or the mesh's vault asks, which the bus
// makes true — it denies these subjects to every principal but those two, and lets the vault alone call them in
// its name — and a caller's own words are refused, so nobody puts a prompt of its choosing before the operator.
func secretQuestion(caller string, args map[string]any) (SecretQuestion, error) {
var q SecretQuestion
for _, free := range []string{"prompt", "message"} {
if _, there := args[free]; there {
@@ -81,14 +93,41 @@ func secretQuestion(args map[string]any) (SecretQuestion, error) {
}
askedBy = v
}
q.Prompt = names["secret"] + " for " + names["module"]
q.Message = fmt.Sprintf("The mesh's controller asks for %s, the own secret of %s on %s.", names["secret"],
names["module"], names["node"])
if askedBy != "" {
q.Message += fmt.Sprintf(" It was asked for by %s.", askedBy)
// What the secret is for, in the catalogue's own words (novox/hq issue 789): the controller's own line, read
// from the secret's declared `prompt` and never a word the caller chose. One line, so it is shown under the
// module and secret names as its own; its characters are never markup, whatever the catalogue holds.
what := ""
if raw, there := args["what"]; there {
v, err := text(args, "what", true)
if err != nil {
return q, err
}
if strings.ContainsAny(v, "\n\r") {
return q, fmt.Errorf("what %q holds more than one line", raw)
}
what = v
}
q.Message += " What you type is not shown, and is sealed before it leaves this machine. Type it only if you " +
q.Prompt = names["secret"] + " for " + names["module"]
asker := "controller"
if theVault(caller) {
asker = "vault"
}
q.Message = fmt.Sprintf("The mesh's %s asks for %s, the own secret of %s on %s.", asker, names["secret"],
names["module"], names["node"])
if what != "" {
q.Message += "\nFor: " + escapeMarkup(what)
}
tail := ""
if askedBy != "" {
tail += fmt.Sprintf(" It was asked for by %s.", askedBy)
}
tail += " What you type is not shown, and is sealed before it leaves this machine. Type it only if you " +
"expect this ask."
if what != "" {
q.Message += "\n" + strings.TrimSpace(tail)
} else {
q.Message += tail
}
key, err := text(args, "seal_to", true)
if err != nil {
return q, err
+74 -8
View File
@@ -30,7 +30,7 @@ func TestWhatIsTypedIsAnsweredSealedToTheAskersKeyAndNeverInTheClear(t *testing.
bin := fakeBinaries(t, map[string]string{"rofi": `echo "$*" > "$LOG"; echo "` + typedValue + `"`})
t.Setenv("LOG", filepath.Join(bin, "log"))
key, pub, priv := aKey(t)
q, err := secretQuestion(map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key})
q, err := secretQuestion("unproven", map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key})
if err != nil {
t.Fatal(err)
}
@@ -62,7 +62,7 @@ func TestWhatIsTypedIsAnsweredSealedToTheAskersKeyAndNeverInTheClear(t *testing.
func TestADismissedEmptyOrLatePromptIsACancelAndAFailureNeverSaysTheValue(t *testing.T) {
withSession(t)
key, _, _ := aKey(t)
q, _ := secretQuestion(map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key, "timeout_seconds": 1})
q, _ := secretQuestion("unproven", map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key, "timeout_seconds": 1})
for script, timedOut := range map[string]bool{`exit 1`: false, `echo ""`: false, `sleep 30`: true} {
fakeBinaries(t, map[string]string{"rofi": script})
got, err := Secret(q)
@@ -92,7 +92,7 @@ func TestASecretIsAskedOnlyWithAKeyToSealItTo(t *testing.T) {
named(map[string]any{"seal_to": key, "timeout_seconds": 60}),
{"seal_to": key},
} {
if _, err := secretQuestion(args); err == nil {
if _, err := secretQuestion("unproven", args); err == nil {
t.Errorf("%v was taken", args)
}
}
@@ -104,7 +104,7 @@ func TestASecretIsAskedOnlyWithAKeyToSealItTo(t *testing.T) {
func TestThePromptIsWrittenHereFromNamesAndSaysTheControllerAsks(t *testing.T) {
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
base := map[string]any{"module": "telegram", "secret": "telegram-token", "node": "anchor", "seal_to": key}
q, err := secretQuestion(base)
q, err := secretQuestion("unproven", base)
if err != nil {
t.Fatal(err)
}
@@ -112,18 +112,26 @@ func TestThePromptIsWrittenHereFromNamesAndSaysTheControllerAsks(t *testing.T) {
strings.Contains(q.Message, "asked for by") {
t.Errorf("prompt %q, message %q", q.Prompt, q.Message)
}
// Asked by the vault's own account, as the runtime proved it (issue 723): the prompt says the vault asks.
if q, err := secretQuestion("novox.mesh-vault", base); err != nil ||
!strings.HasPrefix(q.Message, "The mesh's vault asks for telegram-token") {
t.Errorf("asked by the vault: %q %v", q.Message, err)
}
if q, _ := secretQuestion("novox.mesh-vault-not", base); !strings.HasPrefix(q.Message, "The mesh's controller") {
t.Errorf("a caller that is not the vault's account is said to be the vault: %q", q.Message)
}
// And who asked the controller, when it says so (novox/hq ADR 0277): a caller's name, in a name's characters.
withAsker := map[string]any{}
for k, v := range base {
withAsker[k] = v
}
withAsker["asked_by"] = "g14/claude-code"
q, err = secretQuestion(withAsker)
q, err = secretQuestion("unproven", withAsker)
if err != nil || !strings.Contains(q.Message, "It was asked for by g14/claude-code.") {
t.Errorf("asked_by: %q %v", q.Message, err)
}
withAsker["asked_by"] = "jochen at a shell on novox"
if q, err = secretQuestion(withAsker); err != nil || !strings.Contains(q.Message, "It was asked for by jochen at a shell on novox.") {
if q, err = secretQuestion("unproven", withAsker); err != nil || !strings.Contains(q.Message, "It was asked for by jochen at a shell on novox.") {
t.Errorf("asked_by at a shell: %q %v", q.Message, err)
}
for key, value := range map[string]any{
@@ -139,14 +147,72 @@ func TestThePromptIsWrittenHereFromNamesAndSaysTheControllerAsks(t *testing.T) {
args[k] = v
}
args[key] = value
if _, err := secretQuestion(args); err == nil {
if _, err := secretQuestion("unproven", args); err == nil {
t.Errorf("%s=%q was taken", key, value)
}
}
for _, bad := range []string{"", "a\nb", "a<b>c", "a,b", strings.Repeat("a", 81), "a b", "a:b", "a'b"} {
withAsker["asked_by"] = bad
if _, err := secretQuestion(withAsker); err == nil {
if _, err := secretQuestion("unproven", withAsker); err == nil {
t.Errorf("asked_by=%q was taken", bad)
}
}
}
// What the secret is for shows as its own line under the module and secret names, in the catalogue's own
// words (novox/hq issue 789): read from the secret's declared `prompt`, never a word the caller chose, and
// never shown without a declared one — an old module, or one that declares none, prompts as it does today.
func TestWhatShowsAsItsOwnLineUnderTheNamesWhenDeclaredAndNothingWhenNot(t *testing.T) {
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
base := map[string]any{"module": "mesh-mail", "secret": "mail-credential-gmail", "node": "anchor", "seal_to": key}
q, err := secretQuestion("unproven", base)
if err != nil {
t.Fatal(err)
}
if strings.Contains(q.Message, "For:") {
t.Errorf("a \"For:\" line with no what declared: %q", q.Message)
}
withWhat := map[string]any{}
for k, v := range base {
withWhat[k] = v
}
withWhat["what"] = "the password of the mail account gmail"
q, err = secretQuestion("unproven", withWhat)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(q.Message, "\nFor: the password of the mail account gmail") {
t.Errorf("no \"For:\" line of its own: %q", q.Message)
}
if !strings.Contains(q.Message, "What you type is not shown") {
t.Errorf("the rest of the message was lost: %q", q.Message)
}
}
// What the secret is for is shown as text, never markup (novox/hq issue 789): the prompt's window reads its
// message with markup on, so what the catalogue holds reaches it with the characters markup gives meaning to
// turned to their entities first, and a line holding more than one is refused before anybody sees it.
func TestWhatIsShownAsTextNeverMarkupAndIsOneLine(t *testing.T) {
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
base := map[string]any{"module": "mesh-mail", "secret": "mail-credential-gmail", "node": "anchor", "seal_to": key,
"what": "the password of <b>the</b> mail & account"}
q, err := secretQuestion("unproven", base)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(q.Message, "For: the password of &lt;b&gt;the&lt;/b&gt; mail &amp; account") {
t.Errorf("markup was not escaped: %q", q.Message)
}
for _, bad := range []string{"a\nb", "a\rb"} {
multi := map[string]any{}
for k, v := range base {
multi[k] = v
}
multi["what"] = bad
if _, err := secretQuestion("unproven", multi); err == nil {
t.Errorf("what=%q holding more than one line was taken", bad)
}
}
}