gssh-agent 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,42 +96,65 @@ func needsShell(cmd string) bool {
96
96
  // Connect creates a new SSH session
97
97
  func (m *Manager) Connect(user, host string, port int, password, keyPath string) (*protocol.Session, error) {
98
98
  m.mu.Lock()
99
- defer m.mu.Unlock()
100
99
 
101
100
  // Check if session already exists
102
101
  for _, s := range m.sessions {
103
102
  if s.Host == host && s.User == user && s.Port == port {
104
- if s.Status == "connected" {
105
- return toProtocolSession(s), fmt.Errorf("session already exists")
103
+ s.mu.RLock()
104
+ status := s.Status
105
+ s.mu.RUnlock()
106
+
107
+ if status == "connected" || status == "connecting" || status == "reconnecting" {
108
+ m.defaultID = s.ID
109
+ m.mu.Unlock()
110
+ return toProtocolSession(s), nil
106
111
  }
107
- // Try to reconnect
112
+
113
+ // Mark as connecting to prevent concurrent connect attempts
114
+ s.mu.Lock()
115
+ s.Status = "connecting"
116
+ s.mu.Unlock()
117
+
118
+ // Switch default session
119
+ m.defaultID = s.ID
120
+
121
+ m.mu.Unlock()
122
+
123
+ // Perform network connect outside of Manager lock
108
124
  sshClient, err := client.Connect(user, host, port, password, keyPath)
109
125
  if err != nil {
126
+ s.mu.Lock()
127
+ s.Status = "offline"
128
+ s.mu.Unlock()
110
129
  return nil, err
111
130
  }
131
+
132
+ s.mu.Lock()
133
+ // Check if a disconnect was requested while we were connecting
134
+ if s.Status != "connecting" {
135
+ s.mu.Unlock()
136
+ sshClient.Close()
137
+ return nil, fmt.Errorf("session was disconnected while connecting")
138
+ }
112
139
  s.SSHClient = sshClient
113
140
  s.Status = "connected"
141
+ s.mu.Unlock()
142
+
114
143
  return toProtocolSession(s), nil
115
144
  }
116
145
  }
117
146
 
118
- // Create new session
119
- sshClient, err := client.Connect(user, host, port, password, keyPath)
120
- if err != nil {
121
- return nil, err
122
- }
123
-
124
- id := uuid.New().String()
147
+ // Create new session placeholder
148
+ id := uuid.New().String()[:8]
125
149
  ms := &ManagedSession{
126
- ID: id,
127
- Host: host,
128
- User: user,
129
- Port: port,
130
- Status: "connected",
131
- Password: password,
132
- KeyPath: keyPath,
133
- SSHClient: sshClient,
134
- Forwards: make(map[string]*portforward.Forwarder),
150
+ ID: id,
151
+ Host: host,
152
+ User: user,
153
+ Port: port,
154
+ Status: "connecting",
155
+ Password: password,
156
+ KeyPath: keyPath,
157
+ Forwards: make(map[string]*portforward.Forwarder),
135
158
  }
136
159
 
137
160
  m.sessions[id] = ms
@@ -140,6 +163,31 @@ func (m *Manager) Connect(user, host string, port int, password, keyPath string)
140
163
  if m.defaultID == "" {
141
164
  m.defaultID = id
142
165
  }
166
+ m.mu.Unlock()
167
+
168
+ // Perform network connect outside of Manager lock
169
+ sshClient, err := client.Connect(user, host, port, password, keyPath)
170
+ if err != nil {
171
+ // Clean up the placeholder since initialization failed completely
172
+ m.mu.Lock()
173
+ delete(m.sessions, id)
174
+ if m.defaultID == id {
175
+ m.defaultID = ""
176
+ }
177
+ m.mu.Unlock()
178
+ return nil, err
179
+ }
180
+
181
+ ms.mu.Lock()
182
+ // Check if a disconnect was requested while we were connecting
183
+ if ms.Status != "connecting" {
184
+ ms.mu.Unlock()
185
+ sshClient.Close()
186
+ return nil, fmt.Errorf("session was disconnected while connecting")
187
+ }
188
+ ms.SSHClient = sshClient
189
+ ms.Status = "connected"
190
+ ms.mu.Unlock()
143
191
 
144
192
  // Start reconnect monitor
145
193
  go m.monitorReconnect(ms)
@@ -149,28 +197,32 @@ func (m *Manager) Connect(user, host string, port int, password, keyPath string)
149
197
 
150
198
  // Disconnect closes a session
151
199
  func (m *Manager) Disconnect(sessionID string) error {
200
+ m.mu.Lock()
201
+
152
202
  // Use default session if not specified
153
203
  if sessionID == "" {
154
204
  sessionID = m.defaultID
155
205
  }
156
206
 
157
- m.mu.Lock()
158
- defer m.mu.Unlock()
159
-
160
207
  ms, ok := m.sessions[sessionID]
161
208
  if !ok {
209
+ m.mu.Unlock()
162
210
  return fmt.Errorf("session not found")
163
211
  }
164
212
 
165
- if ms.SSHClient != nil {
166
- ms.SSHClient.Close()
213
+ // Clear default ID when disconnecting
214
+ if m.defaultID == sessionID {
215
+ m.defaultID = ""
167
216
  }
217
+ m.mu.Unlock()
168
218
 
219
+ ms.mu.Lock()
220
+ sshClient := ms.SSHClient
169
221
  ms.Status = "disconnected"
222
+ ms.mu.Unlock()
170
223
 
171
- // Clear default ID when disconnecting
172
- if m.defaultID == sessionID {
173
- m.defaultID = ""
224
+ if sshClient != nil {
225
+ sshClient.Close()
174
226
  }
175
227
 
176
228
  return nil
@@ -191,21 +243,36 @@ func (m *Manager) Reconnect(sessionID string) (*protocol.Session, error) {
191
243
  return nil, fmt.Errorf("session not found")
192
244
  }
193
245
 
246
+ ms.mu.Lock()
247
+ if ms.Status == "connecting" || ms.Status == "reconnecting" {
248
+ ms.mu.Unlock()
249
+ return nil, fmt.Errorf("session is currently connecting")
250
+ }
251
+ existingClient := ms.SSHClient
252
+ ms.Status = "reconnecting"
253
+ ms.mu.Unlock()
254
+
194
255
  // Close existing connection
195
- if ms.SSHClient != nil {
196
- ms.SSHClient.Close()
256
+ if existingClient != nil {
257
+ existingClient.Close()
197
258
  }
198
259
 
199
260
  // Create new connection
200
261
  sshClient, err := client.Connect(ms.User, ms.Host, ms.Port, ms.Password, ms.KeyPath)
201
262
  if err != nil {
202
263
  ms.mu.Lock()
203
- ms.Status = "disconnected"
264
+ ms.Status = "offline"
204
265
  ms.mu.Unlock()
205
266
  return nil, err
206
267
  }
207
268
 
208
269
  ms.mu.Lock()
270
+ // Check if a disconnect was requested while we were reconnecting
271
+ if ms.Status != "reconnecting" {
272
+ ms.mu.Unlock()
273
+ sshClient.Close()
274
+ return nil, fmt.Errorf("session was disconnected while reconnecting")
275
+ }
209
276
  ms.SSHClient = sshClient
210
277
  ms.Status = "connected"
211
278
  ms.mu.Unlock()
@@ -265,18 +332,24 @@ func (m *Manager) Exec(sessionID, command string, timeout int) (*protocol.ExecRe
265
332
  var ok bool
266
333
 
267
334
  if timeout > 0 {
268
- done := make(chan struct{})
335
+ type result struct {
336
+ out []byte
337
+ err error
338
+ }
339
+ done := make(chan result, 1)
269
340
  go func() {
270
- output, err = session.CombinedOutput(fullCmd)
271
- close(done)
341
+ out, err := session.CombinedOutput(fullCmd)
342
+ done <- result{out, err}
272
343
  }()
273
344
  select {
274
- case <-done:
345
+ case res := <-done:
346
+ output = res.out
347
+ err = res.err
275
348
  // 命令执行完成
276
349
  case <-time.After(time.Duration(timeout) * time.Second):
277
350
  session.Signal(ssh.SIGKILL)
278
351
  return &protocol.ExecResult{
279
- Stdout: string(output),
352
+ Stdout: "",
280
353
  Stderr: "",
281
354
  ExitCode: -1,
282
355
  }, fmt.Errorf("command timed out after %d seconds", timeout)
@@ -285,17 +358,19 @@ func (m *Manager) Exec(sessionID, command string, timeout int) (*protocol.ExecRe
285
358
  output, err = session.CombinedOutput(fullCmd)
286
359
  }
287
360
 
361
+ ms.mu.Lock()
362
+ ms.LastCmd = command
363
+ ms.mu.Unlock()
364
+
288
365
  if err != nil {
289
366
  exitErr, ok = err.(*ssh.ExitError)
290
367
  if ok {
291
- ms.LastCmd = command
292
368
  return &protocol.ExecResult{
293
369
  Stdout: string(output),
294
370
  Stderr: "",
295
371
  ExitCode: exitErr.ExitStatus(),
296
372
  }, nil
297
373
  }
298
- ms.LastCmd = command
299
374
  return &protocol.ExecResult{
300
375
  Stdout: string(output),
301
376
  Stderr: "",
@@ -303,7 +378,6 @@ func (m *Manager) Exec(sessionID, command string, timeout int) (*protocol.ExecRe
303
378
  }, nil
304
379
  }
305
380
 
306
- ms.LastCmd = command
307
381
  return &protocol.ExecResult{
308
382
  Stdout: string(output),
309
383
  Stderr: "",
@@ -371,7 +445,7 @@ func (m *Manager) AddForward(sessionID, forwardType string, localPort, remotePor
371
445
  return nil, err
372
446
  }
373
447
 
374
- id := uuid.New().String()
448
+ id := uuid.New().String()[:8]
375
449
  forwarder.ID = id
376
450
 
377
451
  m.forwardMu.Lock()
@@ -598,14 +672,30 @@ func (m *Manager) monitorReconnect(ms *ManagedSession) {
598
672
  return
599
673
  }
600
674
 
601
- if sshClient == nil || sshClient.Client == nil {
675
+ isAlive := false
676
+ if sshClient != nil && sshClient.Client != nil {
677
+ // Try to send a keepalive request
678
+ _, _, err := sshClient.Client.SendRequest("keepalive@gssh", true, nil)
679
+ if err == nil {
680
+ isAlive = true
681
+ }
682
+ }
683
+
684
+ if !isAlive {
602
685
  ms.mu.Lock()
603
686
  ms.Status = "reconnecting"
687
+ if sshClient != nil {
688
+ sshClient.Close()
689
+ ms.SSHClient = nil
690
+ }
604
691
  ms.mu.Unlock()
605
692
 
606
693
  // Try to reconnect
607
694
  newClient, err := client.Connect(ms.User, ms.Host, ms.Port, ms.Password, ms.KeyPath)
608
695
  if err != nil {
696
+ ms.mu.Lock()
697
+ ms.Status = "offline"
698
+ ms.mu.Unlock()
609
699
  continue
610
700
  }
611
701
 
@@ -1,9 +1,126 @@
1
1
  package session
2
2
 
3
3
  import (
4
+ "crypto/rand"
5
+ "crypto/rsa"
6
+ "net"
7
+ "strconv"
4
8
  "testing"
9
+ "time"
10
+
11
+ "golang.org/x/crypto/ssh"
5
12
  )
6
13
 
14
+ // generatePrivateKey generates a new RSA private key for testing
15
+ func generatePrivateKey() (ssh.Signer, error) {
16
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
17
+ if err != nil {
18
+ return nil, err
19
+ }
20
+ return ssh.NewSignerFromKey(key)
21
+ }
22
+
23
+ // startDummySSHServer starts a basic SSH server for testing
24
+ func startDummySSHServer(t *testing.T) (string, int, func()) {
25
+ t.Helper()
26
+
27
+ config := &ssh.ServerConfig{
28
+ PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
29
+ if c.User() == "testuser" && string(pass) == "testpass" {
30
+ return nil, nil
31
+ }
32
+ return nil, ssh.ErrNoAuth
33
+ },
34
+ }
35
+
36
+ signer, err := generatePrivateKey()
37
+ if err != nil {
38
+ t.Fatalf("failed to generate private key: %v", err)
39
+ }
40
+ config.AddHostKey(signer)
41
+
42
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
43
+ if err != nil {
44
+ t.Fatalf("failed to listen on port: %v", err)
45
+ }
46
+
47
+ host, portStr, err := net.SplitHostPort(listener.Addr().String())
48
+ if err != nil {
49
+ t.Fatalf("failed to split host port: %v", err)
50
+ }
51
+
52
+ port, err := strconv.Atoi(portStr)
53
+ if err != nil {
54
+ t.Fatalf("failed to parse port: %v", err)
55
+ }
56
+
57
+ go func() {
58
+ for {
59
+ nConn, err := listener.Accept()
60
+ if err != nil {
61
+ return
62
+ }
63
+
64
+ go func() {
65
+ // Perform SSH handshake
66
+ serverConn, chans, reqs, err := ssh.NewServerConn(nConn, config)
67
+ if err != nil {
68
+ return
69
+ }
70
+
71
+ go ssh.DiscardRequests(reqs)
72
+
73
+ for newChannel := range chans {
74
+ if newChannel.ChannelType() != "session" {
75
+ newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
76
+ continue
77
+ }
78
+
79
+ channel, requests, err := newChannel.Accept()
80
+ if err != nil {
81
+ continue
82
+ }
83
+
84
+ go func() {
85
+ defer channel.Close()
86
+ for req := range requests {
87
+ if req.Type == "exec" {
88
+ // Mock command execution
89
+ if req.WantReply {
90
+ req.Reply(true, nil)
91
+ }
92
+ // Simply write something back and exit
93
+ channel.Write([]byte("mock output"))
94
+
95
+ // Wait a little bit to simulate some work or timeout testing
96
+ cmdLen := len(req.Payload)
97
+ if cmdLen > 4 && string(req.Payload[4:]) == "/bin/sh -c \"sleep 2\"" {
98
+ time.Sleep(2 * time.Second)
99
+ }
100
+ if cmdLen > 4 && string(req.Payload[4:]) == "sleep 2" {
101
+ time.Sleep(2 * time.Second)
102
+ }
103
+
104
+ channel.SendRequest("exit-status", false, ssh.Marshal(struct{ uint32 }{0}))
105
+ return
106
+ }
107
+ }
108
+ }()
109
+ }
110
+
111
+ // Handle client disconnect gracefully if needed
112
+ _ = serverConn
113
+ }()
114
+ }
115
+ }()
116
+
117
+ cleanup := func() {
118
+ listener.Close()
119
+ }
120
+
121
+ return host, port, cleanup
122
+ }
123
+
7
124
  func TestNewManager(t *testing.T) {
8
125
  manager := NewManager()
9
126
  if manager == nil {
@@ -50,3 +167,210 @@ func TestManagerListForwards(t *testing.T) {
50
167
  t.Errorf("expected 0 forwards, got %d", len(forwards))
51
168
  }
52
169
  }
170
+
171
+ func TestNeedsShell(t *testing.T) {
172
+ tests := []struct {
173
+ cmd string
174
+ expected bool
175
+ }{
176
+ {"ls -la", false},
177
+ {"echo hello", false},
178
+ {"cat file.txt | grep test", true},
179
+ {"echo hello > file.txt", true},
180
+ {"cat < file.txt", true},
181
+ {"cat << EOF\nhello\nEOF", true},
182
+ {"echo hello && echo world", true},
183
+ {"echo hello || echo world", true},
184
+ {"sleep 10 &", true},
185
+ {"echo $(whoami)", true},
186
+ {"echo `whoami`", true},
187
+ {"echo ${HOME}", true},
188
+ {"echo $HOME", true},
189
+ {"echo $$", false},
190
+ {"echo hello\nworld", true},
191
+ }
192
+
193
+ for _, tt := range tests {
194
+ t.Run(tt.cmd, func(t *testing.T) {
195
+ result := needsShell(tt.cmd)
196
+ if result != tt.expected {
197
+ t.Errorf("needsShell(%q) = %v; want %v", tt.cmd, result, tt.expected)
198
+ }
199
+ })
200
+ }
201
+ }
202
+
203
+ func TestManagerConnect(t *testing.T) {
204
+ host, port, cleanup := startDummySSHServer(t)
205
+ defer cleanup()
206
+
207
+ manager := NewManager()
208
+
209
+ // Test successful connection
210
+ sess, err := manager.Connect("testuser", host, port, "testpass", "")
211
+ if err != nil {
212
+ t.Fatalf("expected successful connection, got error: %v", err)
213
+ }
214
+
215
+ if sess.Status != "connected" {
216
+ t.Errorf("expected session status 'connected', got %s", sess.Status)
217
+ }
218
+
219
+ // Test duplicate connection (should reuse and return nil error)
220
+ sess2, err := manager.Connect("testuser", host, port, "testpass", "")
221
+ if err != nil {
222
+ t.Errorf("expected successful reuse for duplicate connection, got error: %v", err)
223
+ }
224
+ if sess2.ID != sess.ID {
225
+ t.Errorf("expected reused session ID %s, got %s", sess.ID, sess2.ID)
226
+ }
227
+ }
228
+
229
+ func TestManagerDisconnect(t *testing.T) {
230
+ host, port, cleanup := startDummySSHServer(t)
231
+ defer cleanup()
232
+
233
+ manager := NewManager()
234
+
235
+ // Connect first
236
+ sess, err := manager.Connect("testuser", host, port, "testpass", "")
237
+ if err != nil {
238
+ t.Fatalf("expected successful connection, got error: %v", err)
239
+ }
240
+
241
+ // Disconnect
242
+ err = manager.Disconnect(sess.ID)
243
+ if err != nil {
244
+ t.Errorf("expected successful disconnect, got error: %v", err)
245
+ }
246
+
247
+ // Disconnect non-existent
248
+ err = manager.Disconnect("invalid-id")
249
+ if err == nil {
250
+ t.Errorf("expected error for non-existent session, got nil")
251
+ }
252
+ }
253
+
254
+ func TestManagerReconnect(t *testing.T) {
255
+ host, port, cleanup := startDummySSHServer(t)
256
+ defer cleanup()
257
+
258
+ manager := NewManager()
259
+
260
+ // Connect first
261
+ sess, err := manager.Connect("testuser", host, port, "testpass", "")
262
+ if err != nil {
263
+ t.Fatalf("expected successful connection, got error: %v", err)
264
+ }
265
+
266
+ // Reconnect
267
+ reconnectedSess, err := manager.Reconnect(sess.ID)
268
+ if err != nil {
269
+ t.Errorf("expected successful reconnect, got error: %v", err)
270
+ }
271
+
272
+ if reconnectedSess.Status != "connected" {
273
+ t.Errorf("expected reconnected session status 'connected', got %s", reconnectedSess.Status)
274
+ }
275
+ }
276
+
277
+ func TestManagerConcurrentStress(t *testing.T) {
278
+ host, port, cleanup := startDummySSHServer(t)
279
+ defer cleanup()
280
+
281
+ manager := NewManager()
282
+
283
+ // Connect first
284
+ sess, err := manager.Connect("testuser", host, port, "testpass", "")
285
+ if err != nil {
286
+ t.Fatalf("expected successful connection, got error: %v", err)
287
+ }
288
+
289
+ numGoroutines := 20
290
+ done := make(chan bool)
291
+
292
+ for i := 0; i < numGoroutines; i++ {
293
+ go func(id int) {
294
+ // Randomly perform operations
295
+ if id%3 == 0 {
296
+ manager.Exec(sess.ID, "echo hello", 5)
297
+ } else if id%3 == 1 {
298
+ manager.List()
299
+ } else {
300
+ manager.GetDefaultID()
301
+ }
302
+ done <- true
303
+ }(i)
304
+ }
305
+
306
+ for i := 0; i < numGoroutines; i++ {
307
+ <-done
308
+ }
309
+
310
+ // Make sure we can still disconnect
311
+ err = manager.Disconnect(sess.ID)
312
+ if err != nil {
313
+ t.Errorf("expected successful disconnect after stress, got error: %v", err)
314
+ }
315
+ }
316
+
317
+ func TestManagerConcurrentConnect(t *testing.T) {
318
+ host, port, cleanup := startDummySSHServer(t)
319
+ defer cleanup()
320
+
321
+ manager := NewManager()
322
+
323
+ numGoroutines := 10
324
+ done := make(chan bool)
325
+
326
+ // Since they all try to connect with the same user/host/port, the first one succeeds
327
+ // and subsequent ones should get "session already exists".
328
+ // The key is that no deadlocks happen here.
329
+ for i := 0; i < numGoroutines; i++ {
330
+ go func() {
331
+ manager.Connect("testuser", host, port, "testpass", "")
332
+ done <- true
333
+ }()
334
+ }
335
+
336
+ for i := 0; i < numGoroutines; i++ {
337
+ <-done
338
+ }
339
+
340
+ sessions := manager.List()
341
+ if len(sessions) != 1 {
342
+ t.Errorf("expected exactly 1 session, got %d", len(sessions))
343
+ }
344
+ }
345
+
346
+ func TestManagerExecTimeout(t *testing.T) {
347
+ host, port, cleanup := startDummySSHServer(t)
348
+ defer cleanup()
349
+
350
+ manager := NewManager()
351
+
352
+ sess, err := manager.Connect("testuser", host, port, "testpass", "")
353
+ if err != nil {
354
+ t.Fatalf("expected successful connection, got error: %v", err)
355
+ }
356
+
357
+ // This command takes 2 seconds on our dummy server
358
+ // Set timeout to 1 second
359
+ start := time.Now()
360
+ res, err := manager.Exec(sess.ID, "sleep 2", 1)
361
+
362
+ elapsed := time.Since(start)
363
+
364
+ // Exec returns err != nil when it times out
365
+ if err == nil {
366
+ t.Errorf("expected timeout error, got nil")
367
+ }
368
+
369
+ if res != nil && res.ExitCode != -1 {
370
+ t.Errorf("expected exit code -1 for timeout, got %d", res.ExitCode)
371
+ }
372
+
373
+ if elapsed >= 2*time.Second {
374
+ t.Errorf("expected to timeout before 2 seconds, elapsed: %v", elapsed)
375
+ }
376
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gssh-agent",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "SSH Session Manager for Agents - Stateless SSH client with SFTP support",
5
5
  "bin": {
6
6
  "gssh": "./bin/gssh"
@@ -36,4 +36,4 @@
36
36
  "engines": {
37
37
  "node": ">=14.0.0"
38
38
  }
39
- }
39
+ }
@@ -0,0 +1,36 @@
1
+ package rpc
2
+
3
+ import (
4
+ "encoding/json"
5
+ "testing"
6
+
7
+ "gssh/internal/protocol"
8
+ )
9
+
10
+ func TestHandle_InvalidJSON(t *testing.T) {
11
+ handler := NewHandler(nil)
12
+
13
+ invalidJSON := []byte(`{invalid json`)
14
+
15
+ respBytes, err := handler.Handle(invalidJSON)
16
+ if err != nil {
17
+ t.Fatalf("Handle returned unexpected error: %v", err)
18
+ }
19
+
20
+ var resp protocol.Response
21
+ if err := json.Unmarshal(respBytes, &resp); err != nil {
22
+ t.Fatalf("Failed to unmarshal response: %v", err)
23
+ }
24
+
25
+ if resp.Error == nil {
26
+ t.Fatal("Expected error in response, got nil")
27
+ }
28
+
29
+ if resp.Error.Code != -32700 {
30
+ t.Errorf("Expected error code -32700, got %d", resp.Error.Code)
31
+ }
32
+
33
+ if resp.Error.Message != "Parse error" {
34
+ t.Errorf("Expected error message 'Parse error', got '%s'", resp.Error.Message)
35
+ }
36
+ }
package/plan.md ADDED
@@ -0,0 +1,4 @@
1
+ 1. **Create `pkg/rpc/handler_test.go`:** I will create a test file in the `pkg/rpc` directory.
2
+ 2. **Add test for invalid JSON:** I will add a test `TestHandle_InvalidJSON` that initializes a new `Handler` and calls `Handle` with an invalid JSON byte slice. It will assert that the returned error JSON structure matches the JSON-RPC error response for parse errors (`code: -32700`).
3
+ 3. **Run pre-commit steps:** Complete pre commit steps to ensure proper testing, verifications, reviews and reflections are done.
4
+ 4. **Submit change:** Once everything works and tests pass, I will submit the change.