The module is one Go binary the runtime launches: the renderer (its instruction file held byte for byte to the TypeScript one it replaces), the credentials and identity files, the licence flow of ADR 0206 and the MCP servers in state. Keeps the TypeScript module's key files, so a node moving to it keeps its key. The npm package, its tests and its build go. Both binaries were run together under the real runtime on a test bus with postgres and a stub vendor: a login was adopted by one exchange, the node bound and handed an access token, its file left with no refresh token, and no token in either state.
85 lines
2.4 KiB
Go
85 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"
|
|
"os"
|
|
)
|
|
|
|
// 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 := os.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 := os.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 := os.WriteFile(tmp, raw, 0o600); err != nil {
|
|
return false, err
|
|
}
|
|
return true, os.Rename(tmp, path)
|
|
}
|