diff --git a/go.mod b/go.mod index 1e5c2ca..9c54f20 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/google/jsonschema-go v0.4.3 github.com/gorilla/websocket v1.5.3 github.com/modelcontextprotocol/go-sdk v1.7.0 - github.com/uvwt/agentdock-protocol v0.8.2-0.20260929005641-178b21b7f423 + github.com/uvwt/agentdock-protocol v0.8.2-0.20260929123659-6131ae03b178 go.opentelemetry.io/otel v1.46.0 go.opentelemetry.io/otel/trace v1.46.0 golang.org/x/crypto v0.37.0 diff --git a/go.sum b/go.sum index 31c171e..38c4eab 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfv github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -github.com/uvwt/agentdock-protocol v0.8.2-0.20260929005641-178b21b7f423 h1:rm9gSoFE0mSNnLe21Ejisj2GGPDAYKritl5RAkkEPKY= -github.com/uvwt/agentdock-protocol v0.8.2-0.20260929005641-178b21b7f423/go.mod h1:yoFrGa/mNuAr3b8fupHCFT0b1Kf4Ni/00T4KnwuiFRk= +github.com/uvwt/agentdock-protocol v0.8.2-0.20260929123659-6131ae03b178 h1:fYVKoJx9KiIC3tdA/Q/fufrHSauL+uwHTDnddT3MCQY= +github.com/uvwt/agentdock-protocol v0.8.2-0.20260929123659-6131ae03b178/go.mod h1:yoFrGa/mNuAr3b8fupHCFT0b1Kf4Ni/00T4KnwuiFRk= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= diff --git a/internal/agentdock/nodes.go b/internal/agentdock/nodes.go index 2221de9..f871cff 100644 --- a/internal/agentdock/nodes.go +++ b/internal/agentdock/nodes.go @@ -12,10 +12,10 @@ import ( "fmt" "mime" "net/url" - "regexp" "strings" "time" + protocol "github.com/uvwt/agentdock-protocol" "github.com/uvwt/nexusdock/internal/core" ) @@ -26,8 +26,6 @@ var ( ErrNodeExists = errors.New("AgentDock 设备已经配对") ErrNodeDisabled = errors.New("AgentDock 节点已停用") ErrPairingCodeInvalid = errors.New("AgentDock 配对码无效或已过期") - - deviceIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{7,127}$`) ) type ValidationError struct{ Message string } @@ -58,6 +56,8 @@ type PairingCode struct { ExpiresAt time.Time `json:"expires_at"` } +// PairInput 与 protocol.PairRequest 保持同一字段形状。HTTP 边界通过显式类型转换进入核心层, +// 协议字段发生变化时会在编译期暴露,而不是让两份 JSON 契约静默漂移。 type PairInput struct { Code string `json:"code"` DeviceID string `json:"device_id"` @@ -146,10 +146,10 @@ func (s *Store) Pair(ctx context.Context, input PairInput) (Node, error) { if code == "" { return Node{}, invalid("配对码不能为空") } - if !deviceIDPattern.MatchString(deviceID) { + if !protocol.ValidDeviceID(deviceID) { return Node{}, invalid("设备 ID 格式无效") } - if name == "" || len([]rune(name)) > 100 { + if !protocol.ValidNodeName(name) { return Node{}, invalid("节点名称必须为 1 到 100 个字符") } @@ -202,7 +202,7 @@ func (s *Store) Update(ctx context.Context, id string, input UpdateInput) (Node, args := make([]any, 0, 4) if input.Name != nil { name := strings.TrimSpace(*input.Name) - if name == "" || len([]rune(name)) > 100 { + if !protocol.ValidNodeName(name) { return Node{}, invalid("节点名称必须为 1 到 100 个字符") } updates = append(updates, "name = ?") diff --git a/internal/httpx/agentdock_nodes.go b/internal/httpx/agentdock_nodes.go index ffd4b56..da02ad2 100644 --- a/internal/httpx/agentdock_nodes.go +++ b/internal/httpx/agentdock_nodes.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/go-chi/chi/v5" + protocol "github.com/uvwt/agentdock-protocol" "github.com/uvwt/nexusdock/internal/agentdock" "github.com/uvwt/nexusdock/internal/core" ) @@ -20,6 +21,8 @@ func (s *Server) registerAgentDockAdminRoutes(r chi.Router) { func (s *Server) registerAgentDockConnectionRoutes(r chi.Router) { // 配对码和 Device Token 是这两个入口各自的身份边界,不能套用浏览器会话认证。 + // 路径保留字面量供 contracts 静态扫描;routes_test 会使用 protocol 常量命中真实 Router, + // 因此协议路径一旦变化会由回归测试立即暴露,而不是让两份路径静默漂移。 r.Post("/v1/nodes/pair", s.agentDockNodePair) r.Get("/v1/nodes/connect", s.agentDockNodeConnect) } @@ -58,11 +61,11 @@ func (s *Server) agentDockNodePair(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusServiceUnavailable, "AGENTDOCK_PAIRING_UNAVAILABLE", "AgentDock 配对服务不可用") return } - var request agentdock.PairInput + var request protocol.PairRequest if !decodeJSON(w, r, &request) { return } - node, err := s.agentDock.Pair(r.Context(), request) + node, err := s.agentDock.Pair(r.Context(), agentdock.PairInput(request)) if err != nil { writeAgentDockNodeError(w, err) return @@ -76,7 +79,7 @@ func (s *Server) agentDockNodePair(w http.ResponseWriter, r *http.Request) { } writeJSON(w, http.StatusCreated, map[string]any{ "ok": true, "node": node, "device_token": token, - "connect_path": "/v1/nodes/connect", + "connect_path": protocol.ConnectPath, }) } diff --git a/internal/httpx/agentdock_nodes_test.go b/internal/httpx/agentdock_nodes_test.go index fdc2852..01f188c 100644 --- a/internal/httpx/agentdock_nodes_test.go +++ b/internal/httpx/agentdock_nodes_test.go @@ -12,6 +12,7 @@ import ( "testing" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" + protocol "github.com/uvwt/agentdock-protocol" "github.com/uvwt/agentdock-protocol/mcpcontract" "github.com/uvwt/nexusdock/internal/agentdock" "github.com/uvwt/nexusdock/internal/auth" @@ -48,10 +49,7 @@ func TestPairingIssuesDeviceTokenWithoutAgentDockToken(t *testing.T) { if response.Code != http.StatusCreated { t.Fatalf("status=%d body=%s", response.Code, response.Body.String()) } - var result struct { - Node agentdock.Node `json:"node"` - DeviceToken string `json:"device_token"` - } + var result protocol.PairResponse if err := json.Unmarshal(response.Body.Bytes(), &result); err != nil { t.Fatal(err) } diff --git a/internal/httpx/routes_test.go b/internal/httpx/routes_test.go index 56219ed..5a0269b 100644 --- a/internal/httpx/routes_test.go +++ b/internal/httpx/routes_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/go-chi/chi/v5" + protocol "github.com/uvwt/agentdock-protocol" "github.com/uvwt/nexusdock/internal/agentdock" "github.com/uvwt/nexusdock/internal/core" @@ -46,6 +47,25 @@ func newRouteSecurityTestServer(t *testing.T) (*Server, string) { return server, issued.Token } +func TestRouterServesAgentDockProtocolPathsWithoutWebSession(t *testing.T) { + server := newNodeTestServer(t) + handler := server.Handler() + + pairRequest := httptest.NewRequest(http.MethodPost, protocol.PairPath, strings.NewReader(`{}`)) + pairResponse := httptest.NewRecorder() + handler.ServeHTTP(pairResponse, pairRequest) + if pairResponse.Code != http.StatusBadRequest { + t.Fatalf("POST %s status=%d body=%s, want 400", protocol.PairPath, pairResponse.Code, pairResponse.Body.String()) + } + + connectRequest := httptest.NewRequest(http.MethodGet, protocol.ConnectPath, nil) + connectResponse := httptest.NewRecorder() + handler.ServeHTTP(connectResponse, connectRequest) + if connectResponse.Code != http.StatusUnauthorized { + t.Fatalf("GET %s status=%d body=%s, want 401", protocol.ConnectPath, connectResponse.Code, connectResponse.Body.String()) + } +} + func TestRouterSecurityDomainsKeepDeviceOutOfAdminRoutes(t *testing.T) { server, deviceToken := newRouteSecurityTestServer(t) handler := server.Handler()