From e837a284af70e91cf2c93f176d497932d5332865 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 14 Apr 2026 20:27:44 +0100 Subject: [PATCH] =?UTF-8?q?feat(agent):=20RFC=20=C2=A79=20agentic=5Fauth?= =?UTF-8?q?=5Flogin=20MCP=20tool=20+=20dedupe=20tool=20registrations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pkg/agentic/platform_tools.go | 29 +++++++++++++++++++++++++++++ pkg/agentic/prep.go | 12 ++---------- pkg/agentic/prep_test.go | 6 ++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/pkg/agentic/platform_tools.go b/pkg/agentic/platform_tools.go index a615379..dc68751 100644 --- a/pkg/agentic/platform_tools.go +++ b/pkg/agentic/platform_tools.go @@ -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}, diff --git a/pkg/agentic/prep.go b/pkg/agentic/prep.go index 83bc576..d002cf3 100644 --- a/pkg/agentic/prep.go +++ b/pkg/agentic/prep.go @@ -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() diff --git a/pkg/agentic/prep_test.go b/pkg/agentic/prep_test.go index 2678614..4e14caa 100644 --- a/pkg/agentic/prep_test.go +++ b/pkg/agentic/prep_test.go @@ -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) {