feat(agent): RFC §9 agentic_auth_login MCP tool + dedupe tool registrations

Three load-bearing gaps between the agent RFC and the MCP surface:

- RFC §9 Fleet Mode describes the 6-digit pairing-code bootstrap as the
  primary way an unauthenticated node provisions its first AgentApiKey.
  `handleAuthLogin` existed as an Action but never surfaced as an MCP
  tool, so IDE/CLI callers had to shell out. Adds `agentic_auth_login`
  under `registerPlatformTools` with a thin wrapper over the existing
  handler so the platform contract stays single-sourced.
- `RegisterTools` was double-registering `agentic_scan` (bare
  `mcp.AddTool` before the CORE_MCP_FULL gate, then again via
  `AddToolRecorded` inside the gate). The second call silently replaced
  the first and bypassed tool-registry accounting, so REST bridging and
  metrics saw a zero for scan. Collapses both into a single recorded
  registration before the gate.
- `registerPlanTools` and `registerWatchTool` were also fired twice in
  the CORE_MCP_FULL branch. Removes the duplicates so the extended
  registration list mirrors the always-on list exactly once.
- Switches `agentic_prep_workspace` from bare `mcp.AddTool` to
  `AddToolRecorded` so prep-workspace participates in the same
  accounting as every other RFC §6 tool.

TestPrep_RegisterTools_Good_RegistersCompletionTool now asserts all
three `agentic_auth_*` tools surface, covering the new login registration
alongside provision/revoke.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-14 20:27:44 +01:00
parent b338e12fbf
commit e837a284af
3 changed files with 37 additions and 10 deletions

View file

@ -120,6 +120,11 @@ func (s *PrepSubsystem) registerPlatformTools(svc *coremcp.Service) {
Description: "Revoke a platform API key by key ID.",
}, s.authRevokeTool)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_auth_login",
Description: "Exchange a 6-digit pairing code (generated at app.lthn.ai/device) for an AgentApiKey. Bootstraps a fleet node without requiring an existing API key — RFC §9 Fleet Mode.",
}, s.authLoginTool)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_fleet_register",
Description: "Register a fleet node with models, capabilities, and platform metadata.",
@ -256,6 +261,30 @@ func (s *PrepSubsystem) authRevokeTool(ctx context.Context, _ *mcp.CallToolReque
return nil, output, nil
}
// authLoginTool handles the MCP-side of the RFC §9 pairing-code bootstrap.
// Callers pass a 6-digit pairing code generated at app.lthn.ai/device and
// receive the provisioned AgentApiKey so the node can authenticate future
// platform calls. The code itself is the proof — no existing API key is
// required.
//
// Usage example:
//
// out, _ := clientSession.CallTool(ctx, &mcp.CallToolParams{
// Name: "agentic_auth_login",
// Arguments: json.RawMessage(`{"code": "123456"}`),
// })
func (s *PrepSubsystem) authLoginTool(ctx context.Context, _ *mcp.CallToolRequest, input AuthLoginInput) (*mcp.CallToolResult, AuthLoginOutput, error) {
result := s.handleAuthLogin(ctx, platformOptions(core.Option{Key: "code", Value: input.Code}))
if !result.OK {
return nil, AuthLoginOutput{}, resultErrorValue("agentic.auth.login", result)
}
output, ok := result.Value.(AuthLoginOutput)
if !ok {
return nil, AuthLoginOutput{}, core.E("agentic.auth.login", "invalid auth login output", nil)
}
return nil, output, nil
}
func (s *PrepSubsystem) fleetRegisterTool(ctx context.Context, _ *mcp.CallToolRequest, input FleetNode) (*mcp.CallToolResult, FleetNode, error) {
options := platformOptions(
core.Option{Key: "agent_id", Value: input.AgentID},

View file

@ -595,7 +595,7 @@ func (s *PrepSubsystem) SetCore(c *core.Core) {
// subsystem := agentic.NewPrep()
// subsystem.RegisterTools(svc)
func (s *PrepSubsystem) RegisterTools(svc *coremcp.Service) {
mcp.AddTool(svc.Server(), &mcp.Tool{
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_prep_workspace",
Description: "Prepare an agent workspace: clone repo, create branch, build prompt with context.",
}, s.prepWorkspace)
@ -617,7 +617,7 @@ func (s *PrepSubsystem) RegisterTools(svc *coremcp.Service) {
s.registerWatchTool(svc)
s.registerIssueTools(svc)
s.registerPRTools(svc)
mcp.AddTool(svc.Server(), &mcp.Tool{
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_scan",
Description: "Scan Forge repos for open issues with actionable labels (agentic, help-wanted, bug).",
}, s.scan)
@ -642,14 +642,6 @@ func (s *PrepSubsystem) RegisterTools(svc *coremcp.Service) {
s.registerContentTools(svc)
s.registerLanguageTools(svc)
s.registerSetupTool(svc)
coremcp.AddToolRecorded(svc, svc.Server(), "agentic", &mcp.Tool{
Name: "agentic_scan",
Description: "Scan Forge repos for open issues with actionable labels (agentic, help-wanted, bug).",
}, s.scan)
s.registerPlanTools(svc)
s.registerWatchTool(svc)
}
// subsystem := agentic.NewPrep()

View file

@ -739,6 +739,12 @@ func TestPrep_RegisterTools_Good_RegistersCompletionTool(t *testing.T) {
assert.Contains(t, toolNames, "agent_inbox")
assert.Contains(t, toolNames, "agentic_message_conversation")
assert.Contains(t, toolNames, "agent_conversation")
// RFC §9 pairing-code bootstrap exposes the login flow as an MCP tool so
// IDE/CLI callers can exchange a 6-digit code for an AgentApiKey without
// shelling out.
assert.Contains(t, toolNames, "agentic_auth_login")
assert.Contains(t, toolNames, "agentic_auth_provision")
assert.Contains(t, toolNames, "agentic_auth_revoke")
}
func TestPrep_OnStartup_Good_RegistersGenerateCommand(t *testing.T) {