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.
84 lines
2.4 KiB
Go
84 lines
2.4 KiB
Go
package main
|
|
|
|
// Which account the agent is logged in as (novox/hq ADR 0183): not in the token, but in the agent's own
|
|
// state file beside the home, `~/.claude.json` → `oauthAccount`. Read to report and attribute a login;
|
|
// written, three keys and nothing else, when a licence is switched, so the account Claude Code shows is
|
|
// the one whose token it now holds.
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
)
|
|
|
|
// Identity is an account as the agent's state file names it.
|
|
type Identity struct {
|
|
AccountUUID string `json:"accountUuid"`
|
|
EmailAddress string `json:"emailAddress,omitempty"`
|
|
OrganizationUUID string `json:"organizationUuid,omitempty"`
|
|
}
|
|
|
|
func readState(path string) (map[string]any, bool) {
|
|
raw, err := fsys.ReadFile(path)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
dec := json.NewDecoder(bytes.NewReader(raw))
|
|
dec.UseNumber()
|
|
var m map[string]any
|
|
if dec.Decode(&m) != nil || m == nil {
|
|
return nil, false
|
|
}
|
|
return m, true
|
|
}
|
|
|
|
// ReadIdentity is the account the state file names, or nil — never a guess.
|
|
func ReadIdentity(path string) *Identity {
|
|
m, ok := readState(path)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
a, _ := m["oauthAccount"].(map[string]any)
|
|
uuid, _ := a["accountUuid"].(string)
|
|
if uuid == "" {
|
|
return nil
|
|
}
|
|
id := &Identity{AccountUUID: uuid}
|
|
id.EmailAddress, _ = a["emailAddress"].(string)
|
|
id.OrganizationUUID, _ = a["organizationUuid"].(string)
|
|
return id
|
|
}
|
|
|
|
// WriteIdentity points the state file's account at id, keeping every other key as found; answers whether
|
|
// the file changed. A file that is there and cannot be read as an object is left alone.
|
|
func WriteIdentity(path string, id Identity) (bool, error) {
|
|
m, ok := readState(path)
|
|
if !ok {
|
|
if _, err := fsys.Stat(path); err == nil {
|
|
return false, nil
|
|
}
|
|
m = map[string]any{}
|
|
}
|
|
current, _ := m["oauthAccount"].(map[string]any)
|
|
if current == nil {
|
|
current = map[string]any{}
|
|
}
|
|
e, _ := current["emailAddress"].(string)
|
|
o, _ := current["organizationUuid"].(string)
|
|
if current["accountUuid"] == id.AccountUUID && e == id.EmailAddress && o == id.OrganizationUUID {
|
|
return false, nil
|
|
}
|
|
current["accountUuid"] = id.AccountUUID
|
|
current["emailAddress"] = id.EmailAddress
|
|
current["organizationUuid"] = id.OrganizationUUID
|
|
m["oauthAccount"] = current
|
|
raw, err := indented(m)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
tmp := path + ".mesh-tmp"
|
|
if err := fsys.WriteFile(tmp, raw, 0o600); err != nil {
|
|
return false, err
|
|
}
|
|
return true, fsys.Rename(tmp, path)
|
|
}
|