Give claude-code's runtime its forge credential file, and say a credential it cannot place as a condition (issue 774)

The file gitea-read.secret was declared with no owner, so it was root's at 0600; claude-code's tools run in the
node's runtime as the operator's account and could not read it ("permission denied", ~870 times a machine), said
only in the journal. A file a bundle reads is the runtime account's only when a word of the bundle names it
(catalogue givenTo); this one was found through MESH_CLAUDE_CODE_STATE alone.

Quick fix: name the file with the bundle word MESH_CLAUDE_CODE_FORGE, which the reader now uses. Thorough fix: the
same, plus a failure to place the credential raises a warning condition of the module on the operator-channel seat,
once per reason, cleared when it places again. Chose thorough: the fault ran unseen for days. A change of the
credential now restarts the node's runtime, as for every file a bundle word names.
claude-code's mesh-sdk pin moves to 2da0de75 (main's), the first with asks.Condition.
This commit is contained in:
2026-10-30 18:13:44 +01:00
parent aec2ed3c24
commit 7531c3257d
7 changed files with 162 additions and 15 deletions
+55 -1
View File
@@ -24,6 +24,8 @@ import (
"os"
"path/filepath"
"strings"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// ForgeProvision is the provision whose credential this is, and the file this module's manifest has it delivered to.
@@ -32,7 +34,59 @@ const (
forgeSecret = "gitea-read.secret"
)
func (p Paths) forgeSecret() string { return filepath.Join(p.State, forgeSecret) }
func (p Paths) forgeSecret() string {
if p.Forge != "" {
return p.Forge
}
return filepath.Join(p.State, forgeSecret)
}
// ForgeHealth is what this module says to the operator of its forge credential (the sweep of 2026-10-30, issue 774:
// the file was root's, 0600, and the runtime, the operator's account, could not read it; said only in the journal,
// some 870 times a machine). raised is the reason last raised, "" when none.
type ForgeHealth struct {
Node string
raised string
looked bool
}
// forgeConditionKey is the condition's key: its last part is its kind.
func (f *ForgeHealth) key() string { return "claude-code.machine." + f.Node + ".forge-credential" }
// Look is the condition to say after one placing of the credential, nil when nothing changed: raised on a failure, again
// when its reason changes, cleared when it places again; on the first look a success clears what a run before raised.
func (f *ForgeHealth) Look(err error) *asks.Condition {
first := !f.looked
f.looked = true
subject := asks.Scope{Scope: "machine", ID: f.Node, Machine: f.Node}
if err == nil {
if f.raised == "" && !first {
return nil
}
f.raised = ""
return &asks.Condition{Key: f.key(), Subject: subject, State: asks.StateCleared}
}
why := err.Error()
if why == f.raised {
return nil
}
f.raised = why
needs := "read claude-code's journal on " + f.Node + " and the owner of its forge credential file"
return &asks.Condition{Key: f.key(), Severity: asks.SeverityWarning, State: asks.StateRaised, Subject: subject,
Headline: "Agents on " + f.Node + " cannot use the forge credential",
Summary: why, Needs: needs,
Explanation: asks.NeedsYou + needs + ". " + sentence(why) + " Until it is placed, an agent there cannot " +
"clone or fetch the mesh's private repositories.",
Resolved: "The forge's read credential is placed again on " + f.Node + "."}
}
func sentence(s string) string {
s = strings.TrimSpace(s)
if s == "" || strings.HasSuffix(s, ".") {
return s
}
return strings.ToUpper(s[:1]) + s[1:] + "."
}
// forgeCredentials is where the agent's git reads the credential: under the agent's home, in a directory of the
// mesh's own.
@@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"errors"
"net/http"
"net/http/cgi"
"net/http/httptest"
@@ -10,6 +11,8 @@ import (
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/asks"
)
const agentToken = "0123456789abcdef0123456789abcdef01234567"
@@ -260,3 +263,72 @@ func TestForgeCredentialLinesAreReadStrictly(t *testing.T) {
}
}
}
// The forge credential's file is named by a word of the bundle, so the runtime's account owns it and reads it (the
// sweep of 2026-10-30: root's 0600 file, permission denied ~870 times a machine), and a failure to place it is said to
// the operator as a condition, raised once per reason, cleared when it places again, and cleared at a first good look.
func TestTheForgeCredentialIsTheRuntimesAndItsFailureIsAConditionOfTheModule(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Secrets map[string]string `json:"secrets"`
Build struct {
Artifacts []struct {
Kind string `json:"kind"`
Env map[string]string `json:"env"`
} `json:"artifacts"`
} `json:"build"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
named := false
for _, a := range m.Build.Artifacts {
if a.Kind == "bundle" && a.Env["MESH_CLAUDE_CODE_FORGE"] == m.Secrets[ForgeProvision] && m.Secrets[ForgeProvision] != "" {
named = true
}
}
if !named {
t.Errorf("no word of the bundle names the forge credential's file %q: the runtime cannot read it", m.Secrets[ForgeProvision])
}
p, _ := PathsFrom(envMap{"MESH_CLAUDE_CODE_STATE": "/s", "MESH_CLAUDE_CODE_FORGE": "/s/forge"}.get)
if p.forgeSecret() != "/s/forge" {
t.Errorf("the word is not read: %s", p.forgeSecret())
}
f := &ForgeHealth{Node: "anchor"}
denied := errors.New("the forge's read credential cannot be read: open /s/gitea-read.secret: permission denied")
for i, row := range []struct {
err error
want string // "" nothing said, else the state said
}{
{nil, "cleared"}, // a first good look clears what a run before raised
{nil, ""},
{denied, "raised"},
{denied, ""},
{errors.New("the forge credential could not be written: disk full"), "raised"},
{nil, "cleared"},
{nil, ""},
} {
c := f.Look(row.err)
got := ""
if c != nil {
got = c.State
if _, err := asks.EncodeCondition(*c); err != nil {
t.Errorf("look %d: the condition is refused: %v", i, err)
}
if c.Key != "claude-code.machine.anchor.forge-credential" {
t.Errorf("look %d: key %s", i, c.Key)
}
}
if got != row.want {
t.Errorf("look %d: said %q, want %q", i, got, row.want)
}
}
}
type envMap map[string]string
func (e envMap) get(k string) string { return e[k] }
+24 -1
View File
@@ -22,6 +22,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
func say(format string, args ...any) {
@@ -177,6 +178,19 @@ func (s memoryStateOf) Delete(key string) error { return s.s.Delete(key) }
// memoryCarrier carries this node's memory folders (ADR 0343); nil until the long-running half starts it.
var memoryCarrier *MemoryCarrier
// sayCondition says a condition on the operator-channel seat under this module's name (novox/hq ADR 0437 §9).
func sayCondition(c asks.Condition) error {
subject, err := asks.ConditionSubject("claude-code")
if err != nil {
return err
}
raw, err := asks.EncodeCondition(c)
if err != nil {
return err
}
return stdio.SeatPublish(subject, json.RawMessage(raw), "")
}
// notifyOperator tells the operator through the operator channel's notify verb (ADR 0253).
func notifyOperator(args map[string]any) error {
_, err := ask("seat:operator-channel.notify", args)
@@ -599,12 +613,21 @@ func run(p Paths, view *ServerView, configView *ConfigView) {
// The forge's read credential (forge.go, ADR 0365 §4): placed as held, and put back when git or a person changed
// it; the sessions' environment names it only while one is held.
forgeHeld := ForgeCredentialHeld(p)
forgeHealth := &ForgeHealth{Node: p.Node}
placeForge := func() {
if line, err := PlaceForgeCredential(p); err != nil {
line, err := PlaceForgeCredential(p)
if err != nil {
say("%v", err)
} else if line != "" {
say("%s", line)
}
// Said to the operator as a condition, raised once per reason and cleared when it places again.
if c := forgeHealth.Look(err); c != nil {
if serr := sayCondition(*c); serr != nil {
say("the forge credential's condition could not be said (%s): %v", c.State, serr)
forgeHealth = &ForgeHealth{Node: p.Node} // said again at the next look
}
}
if held := ForgeCredentialHeld(p); held != forgeHeld {
forgeHeld = held
if _, err := RenderNow(p, writeManaged); err != nil {
+5 -2
View File
@@ -54,14 +54,17 @@ func Fingerprint(s string) string {
// AgentHome and OperatorHome are the runtime's words as given, read to judge whose home the home scope writes
// (novox/hq issue 340).
type Paths struct {
State, Facts, Settings, Home, Node string
State, Facts, Settings, Home, Node string
// Forge is the file the forge's read credential arrives in (MESH_CLAUDE_CODE_FORGE): named by a word of the
// bundle, so the runtime's account owns it and can read it (catalogue givenTo); empty is gitea-read.secret in State.
Forge string
Agent, Operator, AgentHome, OperatorHome string
}
// PathsFrom reads them, or answers false outside a launch.
func PathsFrom(env func(string) string) (Paths, bool) {
p := Paths{State: env("MESH_CLAUDE_CODE_STATE"), Facts: env("MESH_CLAUDE_CODE_FACTS"),
Settings: env("MESH_CLAUDE_CODE_SETTINGS"), Home: env("MESH_AGENT_HOME"), Node: env("MESH_NODE"),
Settings: env("MESH_CLAUDE_CODE_SETTINGS"), Forge: env("MESH_CLAUDE_CODE_FORGE"), Home: env("MESH_AGENT_HOME"), Node: env("MESH_NODE"),
Agent: env("MESH_AGENT_ACCOUNT"), Operator: env("MESH_OPERATOR_ACCOUNT"),
AgentHome: env("MESH_AGENT_HOME"), OperatorHome: env("MESH_OPERATOR_HOME")}
if (p.Home == "") != (p.Agent == "") {
+2 -2
View File
@@ -1,7 +1,7 @@
module claude-code
go 1.25.0
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0
require gopkg.in/yaml.v3 v3.0.1
+2 -8
View File
@@ -1,11 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.13 h1:Su4JYpZ+zhNovkGA2DgxiZdcuHpjw2tPJzc/7PljhXg=
git.novox.be/novox/mesh-sdk/go v0.1.13/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629 h1:7TYFV1Q6Mcz+lsLP/sds87tysET4rKCn3rRIQV8gNmA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d h1:HEJyqorKktrkJPeeJdwEo0kdrJjH3fm/YVS9MGftCqY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44 h1:r5xIo5qWOXQdCSMxvqJy8kV4U+P/QWXFK6971Ha6wU8=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013182924-5933eb757d44/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0 h1:lKU3HjNXGiVbLW2hUgFjU/TMTUYMlNnnmZxG5T7biQA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030001116-2da0de75c3f0/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+2 -1
View File
@@ -236,7 +236,8 @@
"env": {
"MESH_CLAUDE_CODE_STATE": "${dir:state}",
"MESH_CLAUDE_CODE_FACTS": "${dir:state}/facts.json",
"MESH_CLAUDE_CODE_SETTINGS": "${dir:state}/settings.json"
"MESH_CLAUDE_CODE_SETTINGS": "${dir:state}/settings.json",
"MESH_CLAUDE_CODE_FORGE": "${dir:state}/gitea-read.secret"
}
},
{