mesh/merge-gate pass: builds claude-code, claude-licence-manager, restic, sudo → ace, g14, novox, shanks; no bus step; every machine composes with the chan…
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
The managed settings apply to every account's claude, so a key-helper script under the agent's home would run as the operator, and through sudo as root: the command is now carried whole in the root-owned managed settings and reads the key only as the agent. The helper reads only a regular file, bounded, without blocking, and a request it does not answer fails in time instead of holding every later one; a directory entry it answers that is not a name is refused. The guard's record is matched on its binary in the query, a half-named agent account is not taken as the operator's, the kept API key stays until the agent's home holds one, restic refuses to restore beside a directory others may write, and the agent-home check cleans a path before judging it.
95 lines
4.2 KiB
Go
95 lines
4.2 KiB
Go
package main
|
|
|
|
// Where the agent runs as an account of its own (novox/hq ADR 0266).
|
|
//
|
|
// The control node names an agent account beside the operator's: agents run there as an account that cannot
|
|
// become root without a person. The controller hands this module both — MESH_AGENT_ACCOUNT and its home, and
|
|
// the operator's — and everything this module writes for the agent goes to the agent's: the licence token, the
|
|
// account it is logged in as, an API key and its helper, the items registered at the home scope (homefs.go).
|
|
//
|
|
// Two things follow when a machine moves its agents to such an account:
|
|
//
|
|
// - **the licence follows at once.** The binding applied is the same, so nothing would fetch the token again
|
|
// until the licence rotates; the agent's new home would hold none. A binding counts as applied only while
|
|
// the account the agent runs as holds it.
|
|
// - **what the module placed in the operator's home goes** (ADR 0182: the mesh owns what it places). The
|
|
// credentials file the module wrote there holds an access token and never a refresh token — so a file that
|
|
// holds no refresh token is the module's, and is removed — only once the agent's own home holds the licence,
|
|
// so a move that has not fetched it yet leaves the operator's in place — with the API key and helper it
|
|
// kept in its state.
|
|
// A file holding a refresh token is a login the person made there: theirs, left, and said. The items placed
|
|
// at the home scope there go as unregistering takes them — each unless the person changed it since.
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// holdsApplied says whether the account the agent runs as holds a licence of this kind.
|
|
func holdsApplied(p Paths, kind string) bool {
|
|
if kind == "api-key" {
|
|
_, err := fsys.Stat(p.apiKey())
|
|
return err == nil
|
|
}
|
|
return GrantOf(ReadCredentials(p.credentials())) != nil
|
|
}
|
|
|
|
// holdsAnyLicence says whether the agent's account holds a token or an API key now.
|
|
func holdsAnyLicence(p Paths) bool {
|
|
return holdsApplied(p, "subscription") || holdsApplied(p, "api-key")
|
|
}
|
|
|
|
// RetireOperatorHome takes back what this module placed for the agent under the operator's home, once the agent
|
|
// runs as an account of its own; answers what it did, or nothing.
|
|
func RetireOperatorHome(p Paths) []string {
|
|
if !p.Confined() || p.OperatorHome == "" || filepath.Clean(p.OperatorHome) == filepath.Clean(p.Home) {
|
|
return nil
|
|
}
|
|
var out []string
|
|
file := filepath.Join(p.OperatorHome, ".claude", ".credentials.json")
|
|
if creds := ReadCredentials(file); creds != nil {
|
|
switch {
|
|
case HoldsLogin(creds):
|
|
out = append(out, "left "+file+": it holds a login the person made, which is theirs; the agent here runs as "+
|
|
p.Agent+" and no longer reads it")
|
|
case GrantOf(creds) != nil && !holdsAnyLicence(p):
|
|
out = append(out, "kept the token this module placed in "+file+" until "+p.Agent+"'s home holds the licence")
|
|
case GrantOf(creds) != nil:
|
|
if err := os.Remove(file); err != nil {
|
|
out = append(out, "the token this module placed in "+file+" could not be removed: "+err.Error())
|
|
} else {
|
|
out = append(out, "removed the token this module placed in "+file+": the agent here runs as "+p.Agent)
|
|
}
|
|
}
|
|
}
|
|
// The items placed at the home scope in the operator's home: each removed unless the person changed it,
|
|
// as unregistering would; they are placed in the agent's home now, from a record of its own.
|
|
record := filepath.Join(p.State, "home-placed.json")
|
|
if _, err := os.Stat(record); err == nil {
|
|
done, left := placeHomeIn(filepath.Join(p.OperatorHome, ".claude"), record, nil)
|
|
for _, line := range done {
|
|
out = append(out, "operator's home "+line)
|
|
}
|
|
for _, line := range left {
|
|
out = append(out, "operator's home "+line)
|
|
}
|
|
var still Placed
|
|
if readJSON(record, &still) && len(still) == 0 {
|
|
_ = os.Remove(record)
|
|
}
|
|
}
|
|
for _, name := range []string{"api-key", "api-key-helper"} {
|
|
old := filepath.Join(p.State, name)
|
|
if name == "api-key" && !holdsApplied(p, "api-key") {
|
|
// Kept, like the token above, until the agent's own home holds the key.
|
|
continue
|
|
}
|
|
if _, err := os.Stat(old); err == nil {
|
|
if err := os.Remove(old); err == nil {
|
|
out = append(out, "removed "+old+": the key and its helper are kept where "+p.Agent+" reads them")
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|