A headless PC can be claimed, and a refused broker says so
Both found by operating the stack rather than writing it: the local processes were OOM-killed and bringing them back hit two gaps. The headless agent had no way to be claimed at all. Bootstrap lived only in desktop/internal/cloud, so the one configuration the agent binary exists for - a back-office PC with no window - could only be onboarded by hand-editing agent.json, which is the state the desktop's Setup screen was built to end. `behavision-agent claim <code>` closes it; the CLI joins its arguments because the code is printed in groups for reading aloud and an operator pasting it will paste the spaces too. Second: after the site's broker password was re-rolled, mosquitto logged "not authorised" while the agent logged "timed out". Those need opposite actions - re-link this PC, or go and look at the network - and paho's SetConnectRetry collapses them, because it retries internally and the connect token never completes. describeStall asks whether a TCP socket opens at all, and says what is known rather than guessing at a reason the broker never gives. Verified end to end: minted a code from the platform as the owner, claimed with the new command, broker connected, and the shop went to online: true with 1/1 cameras on w600k_r50. Also corrects this machine's memory in CLAUDE.md from 16 GB to 8 GB. It feeds the model-fallback reasoning, and the local gallery already holds 17 embeddings tagged w600k_mbf beside 19 tagged w600k_r50 - the fallback has silently fired before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
This commit is contained in:
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/loyaly/behavision-agent/pkg/cameras"
|
||||
"github.com/loyaly/behavision-agent/pkg/config"
|
||||
"github.com/loyaly/behavision-agent/pkg/engine"
|
||||
"github.com/loyaly/behavision-agent/pkg/enrol"
|
||||
"github.com/loyaly/behavision-agent/pkg/mqtt"
|
||||
"github.com/loyaly/behavision-agent/pkg/paths"
|
||||
"github.com/loyaly/behavision-agent/pkg/spool"
|
||||
@@ -38,8 +39,15 @@ var version = "dev"
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "behavision-agent %s\n\nusage: %s <run|status|paths>\n",
|
||||
version, filepath.Base(os.Args[0]))
|
||||
fmt.Fprintf(os.Stderr, `behavision-agent %s
|
||||
|
||||
usage: %s <command>
|
||||
|
||||
run supervise the engine and report to head office (default)
|
||||
claim <code> link this PC to a shop, using an installation code
|
||||
status what this PC is and whether it is claimed
|
||||
paths where this install reads and writes
|
||||
`, version, filepath.Base(os.Args[0]))
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
@@ -53,6 +61,8 @@ func main() {
|
||||
err = cmdRun()
|
||||
case "status":
|
||||
err = cmdStatus()
|
||||
case "claim":
|
||||
err = cmdClaim(flag.Args()[1:])
|
||||
case "paths":
|
||||
err = cmdPaths()
|
||||
default:
|
||||
@@ -64,6 +74,67 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// cmdClaim is the headless half of onboarding.
|
||||
//
|
||||
// The desktop app has had a Setup screen for this; a back-office PC with no
|
||||
// window had nothing at all, so the only way to claim one was to hand-edit
|
||||
// agent.json - which is the state that screen was built to end.
|
||||
func cmdClaim(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("usage: behavision-agent claim <installation code>\n" +
|
||||
"Ask whoever manages your shops for one - they can create it from\n" +
|
||||
"the Behavision platform, under the shop.")
|
||||
}
|
||||
// Joined rather than requiring quotes: the code is printed in groups for
|
||||
// reading aloud, and an operator pasting it will paste the spaces too.
|
||||
code := strings.Join(args, "")
|
||||
|
||||
if err := paths.EnsureState(); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.Load(paths.AgentConfig())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base := cfg.CloudBase
|
||||
if v := os.Getenv("BEHAVISION_CLOUD"); v != "" {
|
||||
base = v
|
||||
}
|
||||
if base == "" {
|
||||
base = "https://mcp.loyaly.ai"
|
||||
}
|
||||
|
||||
b, err := enrol.Claim(context.Background(), base, code)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The slugs, not the uuids: the topic prefix is <client>.<site> and the
|
||||
// broker's ACL is written against exactly that username.
|
||||
cfg.ClientID = b.ClientSlug
|
||||
cfg.SiteID = b.SiteSlug
|
||||
cfg.SiteName = b.SiteName
|
||||
cfg.BrokerURL = b.MQTTURL
|
||||
cfg.BrokerUsername = b.MQTTUser
|
||||
cfg.BrokerPassword = b.MQTTPass
|
||||
cfg.AgentToken = b.AgentToken
|
||||
cfg.CloudBase = base
|
||||
// A PC that was running on its own and has now been linked is no longer
|
||||
// standalone.
|
||||
cfg.Standalone = false
|
||||
if err := cfg.Save(paths.AgentConfig()); err != nil {
|
||||
// Reported, never swallowed: a claim that is not on disk works until
|
||||
// the next restart and then silently is not claimed any more, which
|
||||
// looks exactly like a wrong code.
|
||||
return fmt.Errorf("could not save the settings: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("linked to %s (%s.%s)\n", b.SiteName, b.ClientSlug, b.SiteSlug)
|
||||
fmt.Printf("settings written to %s\n", paths.AgentConfig())
|
||||
fmt.Println("restart the agent for it to take effect.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdPaths() error {
|
||||
return json.NewEncoder(os.Stdout).Encode(map[string]string{
|
||||
"version": version,
|
||||
|
||||
Reference in New Issue
Block a user