On the control node agents are to run as an account that cannot become root (hq ADR 0266). The runtime keeps running as the operator, so the token, the identity and the home-scope items go to the agent's home through a helper started as that account; the licence and the account file follow the move, what was placed in the operator's home goes once the agent holds the licence, and the guard refuses every tool of a session running as anyone else, recording to the journal. An API key is taken only from the agent's home and only when it is one, here and in the licence manager, so the tool cannot carry off the runtime's credential. Unchanged where no agent account is named.
91 lines
4.1 KiB
Go
91 lines
4.1 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 _, 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
|
|
}
|