feat: read agent_id from ctx (SDK now plumbs it)

Plexum-sdk-go now unpacks `_meta.agent_id` from host's MCP tools/call
frames into the ctx. Plugin reads it via plugin.AgentIDFromContext
and uses it for backend api-key resolution (agentKeys map lookup +
bearer token) and HF on_call pre-check.

config.defaultAgentID demoted from "v1 stop-gap for the missing SDK
plumbing" to "fallback for operator-driven plugin-call". Normal turn
loops carry the real caller id through ctx now; defaultAgentID only
fires on host-driven dispatch paths.

Also: log every CallTool dispatch with {tool, agent_id} at info level
so operators can see which agent is hitting which tool without
debug-level chatter.

E2E in sim: PLEXUM_ECHO_FORCE_TOOL_USE drives echo provider →
dialectic_list_topics dispatched via host → plugin sees
agent_id=test-agent in ctx + logs it correctly.
This commit is contained in:
h z
2026-06-03 12:54:53 +01:00
parent c5593e3961
commit 3e02a73c05
2 changed files with 23 additions and 13 deletions

View File

@@ -59,18 +59,20 @@ Restart the host afterwards: `systemctl --user restart plexum`.
| `agentKeys` | `{}` | Per-agent bearer token overrides. | | `agentKeys` | `{}` | Per-agent bearer token overrides. |
| `defaultAgentID` | — | Agent id reported to the backend when host hasn't surfaced one via tool ctx. | | `defaultAgentID` | — | Agent id reported to the backend when host hasn't surfaced one via tool ctx. |
## How agent identity is resolved (v1 limitation) ## How agent identity is resolved
`Dialectic.OpenclawPlugin` got the calling agent's id via the OpenClaw `Dialectic.OpenclawPlugin` got the calling agent's id via the OpenClaw
framework's `ctx.agentId`. The Plexum SDK doesn't yet surface this on framework's `ctx.agentId`. The Plexum SDK now propagates the same
tool dispatch — same constraint `HarborForge.PlexumPlugin` hits. v1 information: the host attaches `_meta.agent_id` to every MCP
falls back to `config.defaultAgentID`. Multi-agent claws can configure `tools/call` frame it dispatches on an agent's behalf, and SDK
`agentKeys` but the bearer token used per call is selected against the serve.go unpacks it into the ctx. Plugins read it via
config default (not the true caller), which is fine for a homogeneous- `plugin.AgentIDFromContext(ctx)`.
role claw but won't sort signed verdicts apart by agent.
The fix (deferred): plumb `AgentContext` through `ToolPlugin.CallTool` `config.defaultAgentID` remains as a fallback used only on host paths
in `Plexum-sdk-go`. Once landed, swap `AgentIDFromCtx` to read it. that genuinely carry no agent context — operator-driven
`plexum plugin-call` invocations for debugging, etc. Leave it empty
in a normal deployment; tool calls coming from real turn loops will
always carry the caller agent id.
## HF on_call coverage pre-check ## HF on_call coverage pre-check
@@ -86,7 +88,6 @@ to make the skip explicit (matches the OpenClaw plugin escape hatch).
## Deferred items ## Deferred items
- **Per-call agent id** — see "How agent identity is resolved" above.
- **HF window-coverage check** — needs a backend-side endpoint or a - **HF window-coverage check** — needs a backend-side endpoint or a
Plexum cross-plugin contract for `harbor-forge` to surface Plexum cross-plugin contract for `harbor-forge` to surface
`HasOnCallCovering(agentID, from, to)`. `HasOnCallCovering(agentID, from, to)`.

View File

@@ -56,9 +56,14 @@ func (p *dialecticPlugin) Init(ctx context.Context, host sdkplugin.HostAPI) erro
Config: p.cfg, Config: p.cfg,
Host: host, Host: host,
AgentIDFromCtx: func(ctx context.Context) string { AgentIDFromCtx: func(ctx context.Context) string {
// v1: SDK doesn't surface the calling agent on tool ctx. // Host attaches the caller agent id via tools/call
// Fall back to the per-claw default. Multi-agent claws will // `_meta.agent_id`; SDK unpacks it into ctx.
// need SDK ctx plumbing — tracked upstream. if id := sdkplugin.AgentIDFromContext(ctx); id != "" {
return id
}
// Fallback for host paths that don't carry an agent
// (CLI plugin-call against this plugin from an operator
// debugging the deployment). Empty when not set.
return p.cfg.DefaultAgentID return p.cfg.DefaultAgentID
}, },
} }
@@ -66,6 +71,10 @@ func (p *dialecticPlugin) Init(ctx context.Context, host sdkplugin.HostAPI) erro
} }
func (p *dialecticPlugin) CallTool(ctx context.Context, name string, input json.RawMessage) (sdkplugin.ToolResult, error) { func (p *dialecticPlugin) CallTool(ctx context.Context, name string, input json.RawMessage) (sdkplugin.ToolResult, error) {
p.host.Log("info", "tools/call dispatched", map[string]any{
"tool": name,
"agent_id": sdkplugin.AgentIDFromContext(ctx),
})
return tools.Dispatch(ctx, p.deps, name, input) return tools.Dispatch(ctx, p.deps, name, input)
} }