feat(kb): per-agent hf-token via HostAPI.GetAgentSecret
Phase 4c. Each KB tool call now picks the right KBClient based on the
calling agent: per-agent hf-token (read via HostAPI.GetAgentSecret)
takes priority, plugin-level Config.APIKey is the fallback for cases
where the agent has no per-agent token set.
Behaviour:
- Agent has hf-token in secrets.enc → use that, KB calls run with
the agent's own HF role
- Agent has no token but plugin-level apiKey is set → fall back
- Neither configured → clear "HF KB backend unavailable" error
surfaces to the model (no silent 401 chain)
internal/tools/kb.go:
- KBDeps.AgentClient(ctx, agentID) callback resolves per-agent
client; nil result means "no per-agent token, try fallback"
- clientForCall picks the right client per tool invocation
- List* tools resolve agentID from sdkplugin.AgentIDFromContext
- Cache tool already had agentID from the dispatch shim
cmd/.../main.go: wires AgentClient closure — host.GetAgentSecret
for "hf-token", kbclient.New(BackendURL, token) if non-empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -207,6 +207,19 @@ func (p *harborForgePlugin) Init(ctx context.Context, host sdkplugin.HostAPI) er
|
||||
return ""
|
||||
},
|
||||
Turn: func(agentID string) int { return 0 }, // best-effort; turn ctx not available to plugins yet
|
||||
AgentClient: func(ctx context.Context, agentID string) (*kbclient.Client, error) {
|
||||
if agentID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
token, err := host.GetAgentSecret(ctx, agentID, "hf-token")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return kbclient.New(p.cfg.BackendURL, token), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,41 @@ import (
|
||||
// KBDeps bundles the dependencies the KB tools need. Caller (main.go)
|
||||
// constructs once and reuses across all tool calls.
|
||||
type KBDeps struct {
|
||||
// Client is the fallback plugin-level KB client (auths with
|
||||
// Config.APIKey). When AgentClient resolves a per-agent client,
|
||||
// that takes priority.
|
||||
Client *kbclient.Client
|
||||
ProfileRoot string // <PLEXUM_PROFILE_ROOT>
|
||||
SessionFor func(agentID string) string // returns sessionID or ""
|
||||
Turn func(agentID string) int // current turn (best-effort, can return 0)
|
||||
HostCallTool func(ctx context.Context, agentID, toolName string, input json.RawMessage) (json.RawMessage, error)
|
||||
|
||||
// AgentClient resolves a per-agent KBClient by reading the agent's
|
||||
// hf-token via HostAPI.GetAgentSecret (decision #20). Returns
|
||||
// (nil, nil) when no token is configured for the agent — caller
|
||||
// then falls back to Client. (nil, err) on RPC / decrypt failure.
|
||||
AgentClient func(ctx context.Context, agentID string) (*kbclient.Client, error)
|
||||
}
|
||||
|
||||
// clientForCall picks the right KBClient: per-agent if AgentClient
|
||||
// returns one, else falls back to the shared plugin-level Client.
|
||||
// Returns (nil, "<reason>") only when neither path produces a usable
|
||||
// client (no configured token + no plugin-level apiKey).
|
||||
func (d KBDeps) clientForCall(ctx context.Context, agentID string) (*kbclient.Client, string) {
|
||||
if d.AgentClient != nil && agentID != "" {
|
||||
c, err := d.AgentClient(ctx, agentID)
|
||||
if err == nil && c != nil {
|
||||
return c, ""
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "agent hf-token lookup failed: " + err.Error()
|
||||
}
|
||||
// no token for this agent — fall through to plugin-level
|
||||
}
|
||||
if d.Client != nil {
|
||||
return d.Client, ""
|
||||
}
|
||||
return nil, "HF KB backend unavailable (no per-agent hf-token + no plugin-level apiKey configured)"
|
||||
}
|
||||
|
||||
// kb tool inputs
|
||||
@@ -72,14 +102,15 @@ type evictResult struct {
|
||||
|
||||
// ToolKBListKBs implements dynamic-kb-list-kbs.
|
||||
func ToolKBListKBs(ctx context.Context, deps KBDeps, raw json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||
if deps.Client == nil {
|
||||
return errResult("dynamic-kb-list-kbs: HF KB backend unavailable")
|
||||
client, reason := deps.clientForCall(ctx, sdkplugin.AgentIDFromContext(ctx))
|
||||
if client == nil {
|
||||
return errResult("dynamic-kb-list-kbs: " + reason)
|
||||
}
|
||||
var in listKBsInput
|
||||
if len(raw) > 0 {
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
}
|
||||
kbs, err := deps.Client.ListKBs(ctx, in.ProjectCode)
|
||||
kbs, err := client.ListKBs(ctx, in.ProjectCode)
|
||||
if err != nil {
|
||||
return errResult("dynamic-kb-list-kbs: " + err.Error())
|
||||
}
|
||||
@@ -97,8 +128,9 @@ func ToolKBListKBs(ctx context.Context, deps KBDeps, raw json.RawMessage) (sdkpl
|
||||
|
||||
// ToolKBListTopics implements dynamic-kb-list-topics.
|
||||
func ToolKBListTopics(ctx context.Context, deps KBDeps, raw json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||
if deps.Client == nil {
|
||||
return errResult("dynamic-kb-list-topics: HF KB backend unavailable")
|
||||
client, reason := deps.clientForCall(ctx, sdkplugin.AgentIDFromContext(ctx))
|
||||
if client == nil {
|
||||
return errResult("dynamic-kb-list-topics: " + reason)
|
||||
}
|
||||
var in listTopicsInput
|
||||
if err := json.Unmarshal(raw, &in); err != nil {
|
||||
@@ -107,7 +139,7 @@ func ToolKBListTopics(ctx context.Context, deps KBDeps, raw json.RawMessage) (sd
|
||||
if in.KBCode == "" {
|
||||
return errResult("dynamic-kb-list-topics: kb-code required")
|
||||
}
|
||||
topics, err := deps.Client.ListTopics(ctx, in.KBCode)
|
||||
topics, err := client.ListTopics(ctx, in.KBCode)
|
||||
if err != nil {
|
||||
return errResult("dynamic-kb-list-topics: " + err.Error())
|
||||
}
|
||||
@@ -123,8 +155,9 @@ func ToolKBListTopics(ctx context.Context, deps KBDeps, raw json.RawMessage) (sd
|
||||
|
||||
// ToolKBListFacts implements dynamic-kb-list-facts.
|
||||
func ToolKBListFacts(ctx context.Context, deps KBDeps, raw json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||
if deps.Client == nil {
|
||||
return errResult("dynamic-kb-list-facts: HF KB backend unavailable")
|
||||
client, reason := deps.clientForCall(ctx, sdkplugin.AgentIDFromContext(ctx))
|
||||
if client == nil {
|
||||
return errResult("dynamic-kb-list-facts: " + reason)
|
||||
}
|
||||
var in listFactsInput
|
||||
if err := json.Unmarshal(raw, &in); err != nil {
|
||||
@@ -133,7 +166,7 @@ func ToolKBListFacts(ctx context.Context, deps KBDeps, raw json.RawMessage) (sdk
|
||||
if in.KBCode == "" || len(in.TopicIDs) == 0 {
|
||||
return errResult("dynamic-kb-list-facts: kb-code + topic-ids required")
|
||||
}
|
||||
facts, err := deps.Client.ListFacts(ctx, in.KBCode, in.TopicIDs)
|
||||
facts, err := client.ListFacts(ctx, in.KBCode, in.TopicIDs)
|
||||
if err != nil {
|
||||
return errResult("dynamic-kb-list-facts: " + err.Error())
|
||||
}
|
||||
@@ -153,8 +186,9 @@ func ToolKBListFacts(ctx context.Context, deps KBDeps, raw json.RawMessage) (sdk
|
||||
// ToolKBCache implements dynamic-kb-cache. Per-agent session lookup
|
||||
// resolves the kb-block.json location.
|
||||
func ToolKBCache(ctx context.Context, deps KBDeps, agentID string, raw json.RawMessage) (sdkplugin.ToolResult, error) {
|
||||
if deps.Client == nil {
|
||||
return errResult("dynamic-kb-cache: HF KB backend unavailable")
|
||||
client, reason := deps.clientForCall(ctx, agentID)
|
||||
if client == nil {
|
||||
return errResult("dynamic-kb-cache: " + reason)
|
||||
}
|
||||
sessionID := ""
|
||||
if deps.SessionFor != nil {
|
||||
@@ -186,7 +220,7 @@ func ToolKBCache(ctx context.Context, deps KBDeps, agentID string, raw json.RawM
|
||||
}
|
||||
var fetched []kbclient.Fact
|
||||
if len(toFetch) > 0 {
|
||||
fetched, err = deps.Client.GetFacts(ctx, in.KBCode, toFetch)
|
||||
fetched, err = client.GetFacts(ctx, in.KBCode, toFetch)
|
||||
if err != nil {
|
||||
return errResult("dynamic-kb-cache: " + err.Error())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user