Skip to content

Commit 29785db

Browse files
CopilotRob-Hague
andauthored
Replace copilot-instructions.md with focused agent instructions; add AGENTS.md
Agent-Logs-Url: https://github.com/sshnet/SSH.NET/sessions/fe219db1-6d98-4cef-bf0b-47b4c0a9a2a5 Co-authored-by: Rob-Hague <5132141+Rob-Hague@users.noreply.github.com>
1 parent 25a931c commit 29785db

File tree

2 files changed

+89
-184
lines changed

2 files changed

+89
-184
lines changed

.github/copilot-instructions.md

Lines changed: 34 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,205 +1,55 @@
1-
# GitHub Copilot Instructions for SSH.NET
1+
# Agent instructions for SSH.NET
22

3-
## What this repository does
3+
## Build
44

5-
SSH.NET is a .NET library for SSH-2 protocol communication, optimized for parallelism. It provides:
5+
Always build in **both** Debug and Release before finalizing changes. Release mode sets `TreatWarningsAsErrors=true`, which will catch StyleCop and analyzer violations that are silent in Debug.
66

7-
- **SSH command execution** (synchronous and async)
8-
- **SFTP** file operations (synchronous and async)
9-
- **SCP** file transfers
10-
- **Port forwarding** (local, remote, dynamic/SOCKS)
11-
- **Interactive shell** via `ShellStream`
12-
- **NetConf** protocol client
13-
- **Multi-factor and certificate-based authentication**
14-
15-
Primary entry points are `SshClient`, `SftpClient`, `ScpClient`, and `NetConfClient`, all extending `BaseClient`.
16-
17-
---
18-
19-
## Core technologies
20-
21-
| Area | Technology |
22-
|---|---|
23-
| Language | C# (`LangVersion=latest`) with `#nullable enable` everywhere |
24-
| Runtimes | .NET Framework 4.6.2, .NET Standard 2.0, .NET 8+, .NET 9+, .NET 10+ |
25-
| Cryptography | BouncyCastle (`BouncyCastle.Cryptography`) |
26-
| Logging | `Microsoft.Extensions.Logging.Abstractions` (`ILogger`/`ILoggerFactory`) |
27-
| Testing | MSTest v4, Moq, Testcontainers (Docker for integration tests) |
28-
| Build tooling | .NET SDK 10.0.100, Nerdbank.GitVersioning |
29-
| Static analysis | StyleCop.Analyzers, Meziantou.Analyzer, SonarAnalyzer.CSharp |
30-
31-
---
32-
33-
## Code organization
34-
35-
```
36-
src/Renci.SshNet/
37-
├── Channels/ # SSH channel types (session, forwarded, X11…)
38-
├── Common/ # Shared utilities, extension methods, custom exceptions
39-
├── Compression/ # Zlib compression support
40-
├── Connection/ # Socket connectors, proxy support (SOCKS4/5, HTTP), key exchange
41-
├── Messages/ # SSH protocol message types
42-
│ ├── Transport/
43-
│ ├── Authentication/
44-
│ └── Connection/
45-
├── Security/ # Algorithms, key-exchange, cryptography wrappers
46-
│ └── Cryptography/ # Ciphers, signatures, key types
47-
├── Sftp/ # SFTP session, requests, responses
48-
├── Netconf/ # NetConf client
49-
└── Abstractions/ # Platform and I/O abstractions
50-
```
51-
52-
Large classes are split into **partial class files** per concern (e.g., `PrivateKeyFile.PKCS1.cs`, `PrivateKeyFile.OpenSSH.cs`).
53-
54-
---
55-
56-
## Naming and style conventions
57-
58-
| Element | Convention | Example |
59-
|---|---|---|
60-
| Classes, methods, properties | PascalCase | `SftpClient`, `ListDirectory()`, `IsConnected` |
61-
| Private fields | `_camelCase` | `_isDisposed`, `_sftpSession` |
62-
| Interfaces | `I` prefix + PascalCase | `ISftpClient`, `IChannel` |
63-
| Constants / static readonly | PascalCase | `MaximumSshPacketSize` |
64-
| Local variables | camelCase | `operationTimeout`, `connectionInfo` |
65-
66-
**StyleCop is enforced.** Follow existing file conventions:
67-
68-
- `#nullable enable` at the top of every `.cs` file.
69-
- `using` directives **outside** the namespace block, grouped with `System.*` first, then a blank line, then other namespaces.
70-
- 4-space indentation (spaces, not tabs).
71-
- XML doc comments (`///`) on all public and internal members; use `<inheritdoc/>` when inheriting from an interface.
72-
- Describe exception conditions in `/// <exception cref="…">` tags.
73-
- No Hungarian notation.
74-
75-
---
76-
77-
## Error handling
78-
79-
Use the existing exception hierarchy; do not throw `Exception` or `ApplicationException` directly.
80-
81-
```
82-
SshException
83-
├── SshConnectionException # connection-level failures
84-
├── SshAuthenticationException # auth failures
85-
├── SshOperationTimeoutException # operation timed out
86-
├── SshPassPhraseNullOrEmptyException
87-
├── ProxyException
88-
├── SftpException
89-
│ ├── SftpPermissionDeniedException
90-
│ └── SftpPathNotFoundException
91-
├── ScpException
92-
└── NetConfServerException
7+
```bash
8+
dotnet build
9+
dotnet build -c Release
9310
```
9411

95-
- Annotate new exception classes with `#if NETFRAMEWORK [Serializable] #endif` and add the protected serialization constructor inside the same `#if` block, matching the pattern in `SshException.cs`.
96-
- Surface async errors by storing them in a `TaskCompletionSource` or re-throwing via `ExceptionDispatchInfo.Throw()` — never swallow exceptions silently.
97-
- Raise `ErrorOccurred` events on long-running components (e.g., `ForwardedPort`, `Session`) rather than propagating the exception across thread boundaries.
98-
99-
---
12+
## Tests
10013

101-
## API design
14+
Run both the unit tests and the integration tests:
10215

103-
- **Every public blocking method should have a `…Async(CancellationToken cancellationToken = default)` counterpart.** Keep both in the same partial class file or co-located partial file.
104-
- Validate all arguments at the top of public methods; prefer `ArgumentNullException`, `ArgumentException`, `ArgumentOutOfRangeException` with descriptive messages.
105-
- Return `IEnumerable<T>` for sequences that are already materialized; use `IAsyncEnumerable<T>` when data streams asynchronously.
106-
- Prefer `ReadOnlyMemory<byte>` / `Memory<byte>` over `byte[]` in new protocol-layer code.
107-
- Do not expose mutable collections directly; use `.AsReadOnly()` or copy-on-return.
108-
109-
---
110-
111-
## Async and concurrency
112-
113-
- Use `async Task` / `async ValueTask` with `CancellationToken` for all new async methods.
114-
- The socket receive loop and keep-alive timer run on dedicated background threads; protect shared state with `lock` or the custom internal `Lock` type used in `Session`.
115-
- Prefer `TaskCompletionSource<T>` to bridge event-driven or callback-based code into the async model.
116-
- Never block inside async code with `.Result` or `.Wait()` — this can cause deadlocks on synchronization-context-bound callers.
117-
- Use `ConfigureAwait(false)` in library code (this is a class library, not an app).
118-
119-
---
120-
121-
## Security-sensitive areas
122-
123-
These areas require extra care:
124-
125-
- **`src/Renci.SshNet/Security/`** — key exchange, algorithm negotiation. Algorithm choices have direct security implications.
126-
- **`src/Renci.SshNet/PrivateKeyFile*.cs`** — key format parsing (PKCS#1, PKCS#8, OpenSSH, PuTTY, ssh.com). Input is untrusted; validate lengths and offsets before indexing.
127-
- **`src/Renci.SshNet/Connection/`** — host key verification. Never bypass host key checking silently.
128-
- **Sensitive data in memory**: clear key material as soon as it is no longer needed; do not log private key bytes or plaintext passwords even at `Debug` level.
129-
- **`SshNetLoggingConfiguration.WiresharkKeyLogFilePath`** is a Debug-only diagnostic that writes session keys to disk. It must never be enabled in production builds.
130-
- **Cryptographic primitives** come from BouncyCastle. Prefer existing wrappers over direct calls to BouncyCastle APIs; adding new algorithms requires corresponding unit tests and algorithm negotiation entries.
131-
132-
---
133-
134-
## Testing
135-
136-
### Unit tests (`test/Renci.SshNet.Tests/`)
137-
138-
- Framework: **MSTest** (`[TestClass]`, `[TestMethod]`, `[TestInitialize]`, `[TestCleanup]`)
139-
- Mocking: **Moq** — use `Mock<T>`, `.Setup(…)`, `.Verify(…)`
140-
- File naming: `{ClassName}Test[_{Scenario}].cs` (e.g., `SftpClientTest.ConnectAsync.cs`)
141-
- Each scenario lives in its own partial class file inside `Classes/`
142-
- Base classes: `TestBase`, `SessionTestBase`, `BaseClientTestBase` — extend the appropriate base
143-
- Arrange-Act-Assert style; each test method is focused on a single observable behaviour
144-
145-
```csharp
146-
[TestMethod]
147-
public void MethodName_Scenario_ExpectedOutcome()
148-
{
149-
// Arrange
150-
var connectionInfo = new PasswordConnectionInfo("host", 22, "user", "pwd");
151-
var target = new SftpClient(connectionInfo);
152-
153-
// Act
154-
var actual = target.SomeProperty;
155-
156-
// Assert
157-
Assert.AreEqual(expected, actual);
158-
}
16+
```bash
17+
dotnet test test/Renci.SshNet.Tests/
18+
dotnet test test/Renci.SshNet.IntegrationTests/
15919
```
16020

161-
### Integration tests (`test/Renci.SshNet.IntegrationTests/`)
162-
163-
- Require **Docker** (via Testcontainers); a running Docker daemon is a prerequisite.
164-
- Run with `dotnet test` like any other project once Docker is available.
21+
Integration tests use **Testcontainers** (Docker required) to spin up an Alpine SSH server automatically.
16522

166-
### Running tests
23+
**On Linux**, skip the `net48` (netfx) TFM — it requires .NET Framework and won't run:
16724

16825
```bash
169-
# Unit tests only
170-
dotnet test test/Renci.SshNet.Tests/
171-
172-
# All tests (requires Docker)
173-
dotnet test
174-
175-
# With code coverage
176-
dotnet test --collect:"XPlat Code Coverage"
26+
dotnet test test/Renci.SshNet.Tests/ --framework net9.0
27+
dotnet test test/Renci.SshNet.IntegrationTests/ --framework net9.0
17728
```
17829

179-
### What to test
30+
For inner-loop speed, testing a single modern .NET target (e.g. `--framework net9.0`) is acceptable when a change affects all targets uniformly.
31+
32+
### Expected pass rate
18033

181-
- Add unit tests for every new public method and every non-trivial internal method.
182-
- Test both the happy path and error/edge cases (e.g., `null` arguments, disposed state, cancellation).
183-
- For async methods, test cancellation via `CancellationToken` and timeout behaviour.
184-
- Do **not** remove or weaken existing tests; add new tests instead.
34+
The baseline is **100% passing**. Occasional failures due to timing or network flakiness are acceptable; treat any failure that could plausibly relate to your change as a real failure and investigate.
18535

186-
---
36+
### Known failures in network-restricted environments
18737

188-
## Performance considerations
38+
The following integration tests require outbound internet access (they connect to `www.google.com:80` or similar) and will fail in network-restricted environments such as the Copilot agent sandbox. These failures are **not** regressions — ignore them:
18939

190-
- SSH.NET is designed for parallelism; avoid introducing `lock` contention on hot paths.
191-
- Prefer `ArrayPool<byte>.Shared` or `MemoryPool<byte>.Shared` over allocating new `byte[]` in protocol encoding/decoding.
192-
- The `BufferSize` on `SftpClient` controls read/write payload sizes; keep protocol overhead in mind when changing defaults.
193-
- Benchmark significant changes with `test/Renci.SshNet.Benchmarks/` using BenchmarkDotNet.
40+
- `Ssh_DynamicPortForwarding_IPv4`
41+
- `Ssh_DynamicPortForwarding_DomainName`
42+
- `Ssh_DynamicPortForwarding_DisposeSshClientWithoutStoppingPort`
43+
- `Ssh_LocalPortForwarding`
44+
- `Ssh_LocalPortForwardingCloseChannels`
45+
- `OldIntegrationTests/ForwardedPortLocalTest` (all methods)
19446

195-
---
47+
## Coding style
19648

197-
## Making changes safely
49+
Style is enforced by **StyleCop** and **Meziantou** analyzers and becomes build errors in Release mode. Follow `.editorconfig` and `stylecop.json` at the repo root. Key rules:
19850

199-
1. **Run the unit tests first** (`dotnet test test/Renci.SshNet.Tests/`) to establish a clean baseline.
200-
2. **Keep changes focused.** Small, targeted PRs merge faster. Separate refactoring from behaviour changes.
201-
3. **Multi-targeting**: the library targets .NET Framework 4.6.2, .NET Standard 2.0, and modern .NET. Use `#if NETFRAMEWORK` / `#if NET` guards for platform-specific code; confirm all TFMs build with `dotnet build`.
202-
4. **Public API changes**: adding new overloads or members is generally safe. Removing or changing existing signatures is a breaking change — discuss in an issue first.
203-
5. **Cryptographic changes**: consult existing algorithm registration lists (in `ConnectionInfo.cs` and `Session.cs`) before adding or removing algorithms.
204-
6. **StyleCop and analyzers**: the build treats analyzer warnings as errors in Release/CI mode. Run `dotnet build -c Release` locally to catch these before pushing.
205-
7. **CI note**: some integration tests can flake due to timing or networking. If an existing (unrelated) test fails intermittently in CI but passes locally, it is likely a known flake.
51+
- `#nullable enable` at the top of every `.cs` file.
52+
- `using` directives **outside** the namespace block; `System.*` namespaces first, blank line, then other namespaces.
53+
- 4-space indentation (spaces, not tabs).
54+
- XML doc comments (`///`) on all public and internal members; `<inheritdoc/>` when implementing an interface.
55+
- Private fields use `_camelCase`; everything else uses `PascalCase`.

AGENTS.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Agent instructions for SSH.NET
2+
3+
## Build
4+
5+
Always build in **both** Debug and Release before finalizing changes. Release mode sets `TreatWarningsAsErrors=true`, which will catch StyleCop and analyzer violations that are silent in Debug.
6+
7+
```bash
8+
dotnet build
9+
dotnet build -c Release
10+
```
11+
12+
## Tests
13+
14+
Run both the unit tests and the integration tests:
15+
16+
```bash
17+
dotnet test test/Renci.SshNet.Tests/
18+
dotnet test test/Renci.SshNet.IntegrationTests/
19+
```
20+
21+
Integration tests use **Testcontainers** (Docker required) to spin up an Alpine SSH server automatically.
22+
23+
**On Linux**, skip the `net48` (netfx) TFM — it requires .NET Framework and won't run:
24+
25+
```bash
26+
dotnet test test/Renci.SshNet.Tests/ --framework net9.0
27+
dotnet test test/Renci.SshNet.IntegrationTests/ --framework net9.0
28+
```
29+
30+
For inner-loop speed, testing a single modern .NET target (e.g. `--framework net9.0`) is acceptable when a change affects all targets uniformly.
31+
32+
### Expected pass rate
33+
34+
The baseline is **100% passing**. Occasional failures due to timing or network flakiness are acceptable; treat any failure that could plausibly relate to your change as a real failure and investigate.
35+
36+
### Known failures in network-restricted environments
37+
38+
The following integration tests require outbound internet access (they connect to `www.google.com:80` or similar) and will fail in network-restricted environments such as the Copilot agent sandbox. These failures are **not** regressions — ignore them:
39+
40+
- `Ssh_DynamicPortForwarding_IPv4`
41+
- `Ssh_DynamicPortForwarding_DomainName`
42+
- `Ssh_DynamicPortForwarding_DisposeSshClientWithoutStoppingPort`
43+
- `Ssh_LocalPortForwarding`
44+
- `Ssh_LocalPortForwardingCloseChannels`
45+
- `OldIntegrationTests/ForwardedPortLocalTest` (all methods)
46+
47+
## Coding style
48+
49+
Style is enforced by **StyleCop** and **Meziantou** analyzers and becomes build errors in Release mode. Follow `.editorconfig` and `stylecop.json` at the repo root. Key rules:
50+
51+
- `#nullable enable` at the top of every `.cs` file.
52+
- `using` directives **outside** the namespace block; `System.*` namespaces first, blank line, then other namespaces.
53+
- 4-space indentation (spaces, not tabs).
54+
- XML doc comments (`///`) on all public and internal members; `<inheritdoc/>` when implementing an interface.
55+
- Private fields use `_camelCase`; everything else uses `PascalCase`.

0 commit comments

Comments
 (0)