Files
mesh-catalog/modules/claude-code/cmd/claude-code/forge.go
T
jschoubben 7531c3257d 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.
2026-10-30 18:13:44 +01:00

197 lines
7.7 KiB
Go

package main
// The forge's read credential in the agent's home (novox/hq ADR 0365 §4, issue 737).
//
// The forge's module mints each node's agent a token of its own account, `agent-<node>`, scoped to read
// repositories alone, and the vault keeps it for this module on this machine alone (provision `gitea-read`, issue
// 749). What arrives in this module's state is git's credential-store lines for it, one per origin the forge is
// reached at. This module places them in the agent's home, a file of mode 0600 written as the agent's account (ADR
// 0266: no other module writes there), and points every session's git at it through the managed settings'
// environment: the file's path, never its content. So an agent clones and fetches what its account may read, and no
// token is printed, typed on a command line or put in an environment a process listing shows.
//
// Git's own `store` helper reads the file; on a token the forge rejects it erases that line, and on a password a
// person typed elsewhere it adds one. Both are undone at the next look (every few seconds): the file holds what the
// forge issued, and nothing else. A value that is not credential lines — one the mesh made while the forge had not
// been asked yet — is not placed, and what was placed before is taken back.
import (
"bytes"
"errors"
"fmt"
"io/fs"
"net/url"
"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.
const (
ForgeProvision = "gitea-read"
forgeSecret = "gitea-read.secret"
)
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.
func (p Paths) forgeCredentials() string {
return filepath.Join(p.Home, ".config", "mesh", "forge.credentials")
}
// ForgeCredentialLines are the credential lines held, checked: each an http(s) origin with an account and a token,
// and nothing else. Anything else is refused, said without the value.
func ForgeCredentialLines(raw string) (string, error) {
var lines []string
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
u, err := url.Parse(line)
if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" || u.User == nil ||
u.User.Username() == "" || u.Path != "" || u.RawQuery != "" || u.Fragment != "" {
return "", errors.New("what the mesh holds for the forge's read credential is not git credential lines " +
"(the forge has not issued it yet, or something else was given)")
}
if pw, ok := u.User.Password(); !ok || pw == "" {
return "", errors.New("a forge credential line names no token")
}
lines = append(lines, line)
}
if len(lines) == 0 {
return "", errors.New("the forge's read credential is empty")
}
return strings.Join(lines, "\n") + "\n", nil
}
// ForgeCredentialHeld says whether this machine holds a forge read credential to place.
func ForgeCredentialHeld(p Paths) bool {
raw, err := os.ReadFile(p.forgeSecret())
if err != nil {
return false
}
_, err = ForgeCredentialLines(string(raw))
return err == nil
}
// PlaceForgeCredential puts the forge's read credential in the agent's home as it is held — written, rewritten when
// it differs, taken back when none is held — and says what it did, or "" when nothing changed. Never the value.
func PlaceForgeCredential(p Paths) (string, error) {
path := p.forgeCredentials()
raw, err := os.ReadFile(p.forgeSecret())
var lines string
var why error
switch {
case errors.Is(err, fs.ErrNotExist):
why = errors.New("this machine holds no forge read credential")
case err != nil:
return "", fmt.Errorf("the forge's read credential cannot be read: %w", err)
default:
lines, why = ForgeCredentialLines(string(raw))
}
if why != nil {
if _, err := fsys.Stat(path); errors.Is(err, fs.ErrNotExist) {
return "", nil
}
if err := fsys.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
return "", fmt.Errorf("the forge credential at %s could not be taken back: %w", path, err)
}
return fmt.Sprintf("forge credential: taken back from %s: %v", path, why), nil
}
if held, err := fsys.ReadFile(path); err == nil && bytes.Equal(held, []byte(lines)) {
if info, err := fsys.Stat(path); err == nil && info.Mode().Perm() == 0o600 {
return "", nil
}
}
if err := fsys.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return "", fmt.Errorf("the directory for the forge credential could not be made: %w", err)
}
tmp := path + ".mesh-tmp"
if err := fsys.WriteFile(tmp, []byte(lines), 0o600); err != nil {
return "", fmt.Errorf("the forge credential could not be written: %w", err)
}
if err := fsys.Chmod(tmp, 0o600); err != nil {
_ = fsys.Remove(tmp)
return "", fmt.Errorf("the forge credential could not be made 0600: %w", err)
}
if err := fsys.Rename(tmp, path); err != nil {
_ = fsys.Remove(tmp)
return "", fmt.Errorf("the forge credential could not be put in place: %w", err)
}
return fmt.Sprintf("forge credential: placed at %s for %d origin(s)", path, strings.Count(lines, "\n")), nil
}
// forgeEnv is what every session's environment carries for git to read the credential: the file's path in git's
// configuration from the environment, never the credential. Laid over the operator's `env`, the mesh's keys winning.
func forgeEnv(managed map[string]any, credentials string) {
if credentials == "" {
return
}
env := map[string]any{}
if held, ok := managed["env"].(map[string]any); ok {
for k, v := range held {
env[k] = v
}
}
env["GIT_CONFIG_COUNT"] = "1"
env["GIT_CONFIG_KEY_0"] = "credential.helper"
env["GIT_CONFIG_VALUE_0"] = "store --file=" + shellQuote(credentials)
managed["env"] = env
}