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.
197 lines
7.7 KiB
Go
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
|
|
}
|