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 }