// plexum-harborforge-plugin — Plexum-side HarborForge plugin. // // Mirrors HarborForge.OpenclawPlugin's responsibilities, recast on the // Plexum SDK: // // - eager activation: plugin spawns at host start so the Monitor // bridge listener and Calendar scheduler are running before any // turn fires // - 9 harborforge_* tools backed by tools.Dispatch // - state-aware wake-agent via HostAPI.WakeAgent for Calendar slots // // Config layout: /plugins/harbor-forge/config.json — see // internal/config/config.go for the schema. package main import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "sync" "time" sdkplugin "git.hangman-lab.top/hzhang/Plexum-sdk-go/plugin" "git.hangman-lab.top/zhi/HarborForge.PlexumPlugin/internal/calendar" hfcfg "git.hangman-lab.top/zhi/HarborForge.PlexumPlugin/internal/config" "git.hangman-lab.top/zhi/HarborForge.PlexumPlugin/internal/monitor" "git.hangman-lab.top/zhi/HarborForge.PlexumPlugin/internal/telemetry" "git.hangman-lab.top/zhi/HarborForge.PlexumPlugin/internal/tools" ) // Version is injected via -ldflags "-X main.Version=…" at build time. var Version = "0.1.0" // harborForgePlugin satisfies sdkplugin.ToolPlugin. type harborForgePlugin struct { host sdkplugin.HostAPI cfg hfcfg.Resolved bridge *monitor.Bridge sched *calendar.Scheduler deps tools.Deps cancelBg context.CancelFunc wg sync.WaitGroup agentCache sync.Map // sessionID/turnID → agentID stash (best-effort) } func (p *harborForgePlugin) Manifest() sdkplugin.Manifest { return manifestFromDisk() } func (p *harborForgePlugin) Init(ctx context.Context, host sdkplugin.HostAPI) error { p.host = host profileRoot := os.Getenv("PLEXUM_PROFILE_ROOT") if profileRoot == "" { home, _ := os.UserHomeDir() profileRoot = filepath.Join(home, ".plexum") } raw, err := hfcfg.Load(profileRoot) if err != nil { return fmt.Errorf("load harbor-forge config: %w", err) } p.cfg = hfcfg.Resolve(raw) host.Log("info", "harbor-forge plugin initialized", map[string]any{ "version": Version, "backend": p.cfg.BackendURL, "identifier": p.cfg.Identifier, "monitor_port": p.cfg.MonitorPort, "calendar_enabled": p.cfg.CalendarEnabled, }) bgCtx, cancel := context.WithCancel(context.Background()) p.cancelBg = cancel // Listers + collectors capture bgCtx (not Init ctx) — Init returns // once MCP initialize completes, but the plugin process lives on // and so do the goroutines + closures we registered. collect := func() telemetry.Snapshot { return telemetry.Collect(telemetry.CollectOpts{ Identifier: p.cfg.Identifier, Version: Version, AgentLister: func() []telemetry.AgentInfo { return p.listAgents(bgCtx, profileRoot) }, }) } p.bridge = monitor.New(p.cfg.MonitorPort, collect, func(level, msg string, attrs map[string]any) { host.Log(level, msg, attrs) }) if err := p.bridge.Start(bgCtx); err != nil { host.Log("warn", "monitor bridge failed to start", map[string]any{"err": err.Error()}) } calBackend := p.cfg.CalendarBackendURL if calBackend == "" { calBackend = p.cfg.BackendURL } bridge := calendar.New(calBackend, p.cfg.Identifier) p.sched = calendar.NewScheduler( calendar.Config{ HeartbeatInterval: time.Duration(p.cfg.CalendarHeartbeatIntervalSeconds) * time.Second, }, bridge, host, calendar.PluginInfoTag{Name: "harbor-forge", Version: Version, Backend: "plexum"}, func() []calendar.ReportableAgent { return p.listReportableAgents(bgCtx, profileRoot) }, ) if p.cfg.CalendarEnabled { p.wg.Add(1) go func() { defer p.wg.Done() if err := p.sched.Run(bgCtx); err != nil { host.Log("warn", "calendar scheduler exited", map[string]any{"err": err.Error()}) } }() } else { host.Log("info", "calendar scheduler disabled by config", nil) } p.deps = tools.Deps{ Config: p.cfg, Version: Version, Collect: collect, Bridge: p.bridge, Scheduler: p.sched, Host: host, AgentIDFromCtx: func(ctx context.Context) string { // Plexum stashes the calling agent id on the host-side // context (via WithAgent) before dispatching tool calls. // We can't directly import internal/agentloop from a // plugin, so we rely on PLEXUM_TOOL_AGENT_ID env-style // (set per-call by host when we add that wiring) or fall // back to the only-active-agent heuristic. v1: prefer the // only-active wake-target (deterministic in single-agent // HF deployments). return p.bestEffortAgentID() }, } return nil } func (p *harborForgePlugin) CallTool(ctx context.Context, name string, input json.RawMessage) (sdkplugin.ToolResult, error) { return tools.Dispatch(ctx, p.deps, name, input) } // ---- agent enumeration ---- // listAgents walks /agents/*/agent.json + state.json so the // telemetry payload includes every Plexum agent visible on this host. // Best-effort: read failures degrade to empty list. func (p *harborForgePlugin) listAgents(ctx context.Context, profileRoot string) []telemetry.AgentInfo { root := filepath.Join(profileRoot, "agents") entries, err := os.ReadDir(root) if err != nil { return nil } out := make([]telemetry.AgentInfo, 0, len(entries)) for _, e := range entries { if !e.IsDir() { continue } agentID := e.Name() var info telemetry.AgentInfo info.ID = agentID if raw, err := os.ReadFile(filepath.Join(root, agentID, "agent.json")); err == nil { var meta struct{ Model string `json:"model"` } _ = json.Unmarshal(raw, &meta) info.Model = meta.Model } // State via HostAPI.ReadAgentState — host-side, ground truth. if snap, err := p.host.ReadAgentState(ctx, agentID); err == nil { info.State = snap.State } out = append(out, info) } return out } func (p *harborForgePlugin) listReportableAgents(ctx context.Context, profileRoot string) []calendar.ReportableAgent { telem := p.listAgents(ctx, profileRoot) out := make([]calendar.ReportableAgent, 0, len(telem)) for _, a := range telem { out = append(out, calendar.ReportableAgent{ ID: a.ID, Model: a.Model, State: mapStateToCalendar(a.State), }) } return out } func mapStateToCalendar(s string) calendar.AgentStatusValue { switch strings.ToLower(s) { case "idle": return calendar.AgentStatusIdle case "working": return calendar.AgentStatusOnCall case "busy": return calendar.AgentStatusBusy case "offline": return calendar.AgentStatusOffline } return calendar.AgentStatusOffline } // bestEffortAgentID is a v1 stop-gap for tools that need the calling // agent's id but don't have it on the ctx (Plexum SDK doesn't yet // expose this — TODO upstream). v1: if exactly one agent has an // active calendar slot we return it; otherwise empty. The calendar // tools (the only ones that need agent context) usually fire when // exactly one slot is active. func (p *harborForgePlugin) bestEffortAgentID() string { sch := p.sched.Status() if len(sch.Active) != 1 { return "" } // We don't track AgentID on Slot directly — the scheduler keeps // activeByAgentID. Iterate to find the one. for _, a := range sch.Active { // Slot is shared between agents only via the scheduler's maps; // here we have just the Slot struct without owner. _ = a } // Fallback to scheduler's helper: return p.sched.SingleActiveAgentID() } func manifestFromDisk() sdkplugin.Manifest { // Bundled manifest.json is the authoritative shape; the binary // version reads it next to itself to avoid hand-syncing two // definitions. Falls back to a minimal in-code manifest if the // file is missing (development / first build). exe, err := os.Executable() if err == nil { raw, err := os.ReadFile(filepath.Join(filepath.Dir(exe), "manifest.json")) if err == nil { var m sdkplugin.Manifest if err := json.Unmarshal(raw, &m); err == nil && m.Name != "" { return m } } } return sdkplugin.Manifest{ Name: "harbor-forge", Version: Version, Activation: sdkplugin.ActivationEager, Executable: "plexum-harborforge-plugin", } } func main() { p := &harborForgePlugin{} defer func() { if p.cancelBg != nil { p.cancelBg() } p.wg.Wait() }() if err := sdkplugin.Serve(p); err != nil && !errors.Is(err, context.Canceled) { fmt.Fprintf(os.Stderr, "plexum-harborforge-plugin: %v\n", err) os.Exit(1) } }