Skip to content

Commit 4233d52

Browse files
authored
Merge pull request #157 from petrsnd/feature/modular-agent-context
Reduce AGENTS.md and use skills instead
2 parents 5b0471f + 4a53820 commit 4233d52

4 files changed

Lines changed: 688 additions & 542 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
name: architecture
3+
description: >-
4+
Use when working on SDK internals, authentication flows, connection
5+
classes, event listeners, A2A contexts, SPS integration, or adding
6+
new authentication methods. Covers the rSTS token exchange, strategy
7+
pattern, and SignalR event system.
8+
---
9+
10+
# Architecture Deep Dive
11+
12+
## Entry point (`Safeguard.java`)
13+
14+
The static `Safeguard` class is the SDK's public entry point. All SDK usage starts through
15+
static factory methods:
16+
17+
- **`Safeguard.connect(...)`** — Creates `ISafeguardConnection` instances. Multiple
18+
overloads support password, certificate (keystore/file/thumbprint/byte array), and
19+
access token authentication.
20+
- **`Safeguard.A2A.getContext(...)`** — Creates `ISafeguardA2AContext` for
21+
application-to-application credential retrieval. Only supports certificate authentication.
22+
- **`Safeguard.A2A.Events.getPersistentA2AEventListener(...)`** — Creates auto-reconnecting
23+
A2A event listeners.
24+
- **`Safeguard.Persist(connection)`** — Wraps any connection in a
25+
`PersistentSafeguardConnection` that auto-refreshes tokens.
26+
- **`SafeguardForPrivilegedSessions.Connect(...)`** — Creates
27+
`ISafeguardSessionsConnection` for Safeguard for Privileged Sessions (SPS).
28+
29+
## Safeguard API services
30+
31+
The SDK targets five backend services, represented by the `Service` enum:
32+
33+
| Service | Endpoint pattern | Auth required |
34+
|---|---|---|
35+
| `Core` | `/service/core/v{version}` | Yes |
36+
| `Appliance` | `/service/appliance/v{version}` | Yes |
37+
| `Notification` | `/service/notification/v{version}` | No |
38+
| `A2A` | `/service/a2a/v{version}` | Certificate |
39+
| `Management` | `/service/management/v{version}` | Yes |
40+
41+
## Authentication strategy pattern (`authentication/`)
42+
43+
All authenticators implement `IAuthenticationMechanism`. When adding a new authentication
44+
method:
45+
1. Implement `IAuthenticationMechanism` in the `authentication/` package
46+
2. Add `Safeguard.connect()` overload(s) in `Safeguard.java`
47+
3. Follow the pattern of existing authenticators (extend `AuthenticatorBase`)
48+
49+
### Adding a new authentication method — checklist
50+
51+
1. **Create the authenticator class** in `src/.../authentication/`:
52+
- Extend `AuthenticatorBase` (provides rSTS token exchange logic)
53+
- Implement `IAuthenticationMechanism` interface methods:
54+
- `getId()` — unique identifier string
55+
- `getAccessToken()` — obtain token via rSTS
56+
- `refreshAccessToken()` — refresh existing token
57+
- `getAccessTokenLifetimeRemaining()` — seconds until expiry
58+
- `logout()` — revoke token and clean up
59+
- `dispose()` — release resources
60+
- Follow the `char[]` convention for any credential parameters
61+
62+
2. **Add factory overloads** in `Safeguard.java`:
63+
- Add static `connect()` method(s) with appropriate parameters
64+
- Include overloads for both `ignoreSsl` boolean and `HostnameVerifier` callback
65+
- Include overloads with and without `apiVersion` parameter
66+
67+
3. **Wire up connection creation**:
68+
- Instantiate the authenticator, call `connect()` on it
69+
- Wrap in `SafeguardConnection` and return as `ISafeguardConnection`
70+
71+
4. **Add tests** — see the testing-guide skill for module-to-suite mapping
72+
73+
## Connection classes
74+
75+
- **`SafeguardConnection`** — Base `ISafeguardConnection` implementation. Makes HTTP calls
76+
via `invokeMethod()` / `invokeMethodFull()`.
77+
- **`PersistentSafeguardConnection`** — Decorator that checks
78+
`getAccessTokenLifetimeRemaining() <= 0` before each call and auto-refreshes tokens.
79+
80+
### Token refresh flow in `PersistentSafeguardConnection`
81+
82+
```
83+
Client calls invokeMethod()
84+
→ PersistentSafeguardConnection checks getAccessTokenLifetimeRemaining()
85+
→ If <= 0: calls refreshAccessToken() on the underlying authenticator
86+
→ Delegates to SafeguardConnection.invokeMethod() with refreshed token
87+
```
88+
89+
The decorator is transparent — callers interact with `ISafeguardConnection` identically
90+
regardless of whether persistence is enabled.
91+
92+
## rSTS authentication flow
93+
94+
All authenticators obtain tokens via the embedded Safeguard RSTS (Resource Security Token
95+
Service) at `https://{host}/RSTS/oauth2/token`.
96+
97+
- **Password authentication** uses the `password` grant type (Resource Owner Grant). **ROG
98+
is disabled by default** on modern Safeguard appliances. If ROG is disabled, password
99+
auth will fail with a 400 error. It must be explicitly enabled via appliance settings
100+
or the test framework's preflight check.
101+
- **PKCE authentication** (`PasswordAuthenticator` with `usePkce=true`) drives the rSTS
102+
login controller at `/RSTS/UserLogin/LoginController` programmatically, exchanging an
103+
authorization code for a token. **PKCE is always available** regardless of ROG settings
104+
and is the preferred method for interactive/programmatic login.
105+
- **Certificate authentication** uses the `client_credentials` grant type with a client
106+
certificate.
107+
- **Access token authentication** accepts a pre-obtained token but cannot refresh it.
108+
109+
The test framework handles ROG automatically — it enables ROG before tests run and
110+
restores the original setting when tests complete. Both ROG and PKCE are valid for tests;
111+
use whichever is appropriate for the feature being tested.
112+
113+
## Event listeners (`event/`)
114+
115+
- **`SafeguardEventListener`** — Standard SignalR listener. Does NOT survive prolonged outages.
116+
- **`PersistentSafeguardEventListener`** — Auto-reconnecting persistent listener.
117+
- **`PersistentSafeguardA2AEventListener`** — Persistent A2A-specific variant.
118+
- **`EventHandlerRegistry`** — Thread-safe handler dispatch. Each event type gets its own
119+
handler thread; handlers for the same event execute sequentially, handlers for different
120+
events execute concurrently.
121+
- Use `getPersistentEventListener()` for production deployments.
122+
- Event handling code **must use Gson** `JsonElement`/`JsonObject` types (transitive from
123+
the SignalR Java client's `GsonHubProtocol`).
124+
125+
### Gson vs Jackson in event handlers (common mistake)
126+
127+
The SignalR Java client uses `GsonHubProtocol`, which means all event payloads arrive as
128+
Gson `JsonElement` objects. Although the SDK uses Jackson Databind for REST API responses,
129+
event handler code must use Gson types:
130+
131+
```java
132+
// CORRECT — Gson types for event payloads
133+
import com.google.gson.JsonElement;
134+
import com.google.gson.JsonObject;
135+
136+
handler.setHandler(event -> {
137+
JsonObject payload = event.getAsJsonObject();
138+
String name = payload.get("Name").getAsString();
139+
});
140+
141+
// WRONG — Jackson types will fail at runtime
142+
import com.fasterxml.jackson.databind.JsonNode; // Don't use for events
143+
```
144+
145+
## A2A (`SafeguardA2AContext`)
146+
147+
Certificate-only authentication for automated credential retrieval. Key types:
148+
`ISafeguardA2AContext`, `A2ARegistration`, `BrokeredAccessRequest`.
149+
150+
A2A contexts use client certificates to authenticate directly — no username/password
151+
involved. The certificate must be registered as an A2A credential retrieval certificate
152+
on the appliance.
153+
154+
## SPS integration (`SafeguardForPrivilegedSessions`)
155+
156+
Integration with Safeguard for Privileged Sessions. `ISafeguardSessionsConnection` /
157+
`SafeguardSessionsConnection`. Connects using basic auth (username/password) over HTTPS.
158+
159+
SPS has its own REST API at `https://<sps-address>/api/`. The `ISafeguardSessionsConnection`
160+
interface provides `invokeMethod()` / `invokeMethodFull()` similar to the main connection,
161+
but targets the SPS API instead of the SPP API.
162+
163+
## Certificate handling (`CertificateContext`)
164+
165+
The `CertificateContext` class unifies multiple certificate input formats:
166+
167+
| Input format | Method/Constructor | Notes |
168+
|---|---|---|
169+
| JKS keystore | File path + password | Standard Java keystore |
170+
| PFX/PKCS#12 file | File path + password | Cross-platform format |
171+
| Byte array | Raw bytes + password | For certificates stored in memory or database |
172+
| Windows thumbprint | Certificate thumbprint string | Windows certificate store lookup |
173+
174+
When working with certificate authentication, ensure:
175+
- PFX files include the private key (required for client certificate auth)
176+
- The certificate chain is complete (or the CA is trusted by the appliance)
177+
- Passwords are passed as `char[]`, not `String`
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
name: ci-cd-pipeline
3+
description: >-
4+
Use when modifying Azure Pipelines, build templates, signing
5+
configuration, Maven Central publishing, GitHub Packages publishing,
6+
or release process. Covers GPG signing, JAR code signing, version
7+
strategy, and critical pipeline pitfalls.
8+
---
9+
10+
# CI/CD Pipeline
11+
12+
The project uses **Azure Pipelines** (`azure-pipelines.yml`) with shared templates in
13+
`pipeline-templates/`.
14+
15+
## Pipeline architecture
16+
17+
The pipeline has **two jobs**, matching the SafeguardDotNet pattern:
18+
19+
| Job | Runs when | What it does |
20+
|---|---|---|
21+
| **PRValidation** | Pull requests only | `mvn package` — compile, lint, SpotBugs |
22+
| **BuildAndPublish** | Merges to `master`/`release-*` | Build, GPG sign, JAR sign, publish to Maven Central + GitHub Packages, create GitHub Release |
23+
24+
Both jobs share `build-steps.yml` with different parameters. PRValidation uses defaults
25+
(`package`, no signing); BuildAndPublish passes `deploy`, release profile flags, and
26+
`signJars: true`.
27+
28+
## Pipeline template files
29+
30+
| File | Scope | Contents |
31+
|---|---|---|
32+
| `global-variables.yml` | Pipeline-level | `semanticVersion`, `isPrerelease`, `versionSuffix` |
33+
| `job-variables.yml` | Job-level | `version` (composed), `targetDir`, `gpgKeyName` |
34+
| `build-steps.yml` | Steps template | Maven task + optional Docker JAR signing + artifact publishing |
35+
36+
## Version strategy
37+
38+
Version is composed from runtime variables: `$(semanticVersion).$(Build.BuildId)$(versionSuffix)`
39+
40+
- When `isPrerelease: 'true'``versionSuffix` is `-SNAPSHOT` → e.g. `8.2.0.355537-SNAPSHOT`
41+
- When `isPrerelease: 'false'``versionSuffix` is empty → e.g. `8.2.0.355537`
42+
43+
The pom.xml uses `<version>${revision}</version>` with CI-friendly properties. The actual
44+
version is always injected via `-Drevision=$(version)` on the Maven command line.
45+
46+
## Service connections and key vaults
47+
48+
| Service connection | Key vault | Secrets |
49+
|---|---|---|
50+
| `SafeguardOpenSource` | `SafeguardBuildSecrets` | `SonatypeUserToken`, `SonatypeRepositoryPassword`, `GpgCodeSigningKey`, `SigningStorePassword`, `GitHubPackagesToken` |
51+
| `OneIdentity.Infrastructure.SPPCodeSigning` | `SPPCodeSigning` | `SPPCodeSigning-Password`, `SPPCodeSigning-TotpPrivateKey` |
52+
| `PangaeaBuild-GitHub` | *(none)* | GitHub service connection for Release creation |
53+
54+
## Two signing mechanisms
55+
56+
The pipeline uses **two independent signing mechanisms** that serve different purposes:
57+
58+
### 1. GPG signing (`maven-gpg-plugin`)
59+
60+
Produces `.asc` detached signature files required by Maven Central for release validation.
61+
Configured in the `release` Maven profile.
62+
63+
- GPG private key: vault secret `GpgCodeSigningKey`, imported via `gpg --batch --import`
64+
- GPG passphrase: vault secret `SigningStorePassword`
65+
- `settings/settings.xml` maps the passphrase via a server entry:
66+
`<id>${gpgkeyname}</id>` + `<passphrase>${signingkeystorepassword}</passphrase>`
67+
- Maven command line needs both `-Dsigningkeystorepassword=$(SigningStorePassword)` and
68+
`-Dgpgkeyname=$(gpgKeyName)`
69+
- The plugin uses `--batch --pinentry-mode loopback` for non-interactive passphrase input
70+
- **CRITICAL:** The GPG plugin is bound to the `verify` lifecycle phase. `mvn package`
71+
stops at the `package` phase and **never triggers GPG signing**. Only `mvn deploy`
72+
(or `mvn verify` / `mvn install`) reaches the GPG plugin. This means you cannot test
73+
GPG signing with `mvn package -P release` — it will succeed silently without signing.
74+
75+
### 2. JAR code signing (SSL.com CodeSigner Docker image)
76+
77+
Embeds certificates in `META-INF/` (CERT.SF, CERT.RSA) inside the JAR for Java runtime
78+
signature verification.
79+
80+
- Uses Docker image `ghcr.io/sslcom/codesigner:latest` which bundles its own JRE
81+
- eSigner account: `ssl.oid.safeguardpp@groups.quest.com`
82+
- Credentials: `SPPCodeSigning-Password` (password) + `SPPCodeSigning-TotpPrivateKey` (TOTP)
83+
- The `-override` flag signs the JAR **in place** (no separate output file)
84+
- `ENVIRONMENT_NAME=PROD` is required for production signing (vs sandbox)
85+
- No `CREDENTIAL_ID` is needed when the eSigner account has only one certificate
86+
- Runs as a **post-build step** after Maven completes, not as a Maven plugin
87+
88+
## CodeSignTool pitfalls (IMPORTANT)
89+
90+
**Do NOT attempt to run CodeSignTool directly on the build agent.** The standalone
91+
CodeSignTool v1.3.2 is compiled for Java 11 and has strict JVM compatibility requirements:
92+
93+
- **Java 8**`UnsupportedClassVersionError` (class file version 55.0, needs Java 11+)
94+
- **Java 11** → Works, but requires a separate JDK install on the build agent
95+
- **Java 17+**`IllegalAccessError` due to JPMS module access restrictions
96+
97+
The **Docker image is the correct approach** — it bundles a compatible JRE inside the
98+
container, isolating CodeSignTool from the host Java version entirely. The build agent
99+
only needs Docker installed (which Azure Pipelines `ubuntu-latest` provides by default).
100+
101+
## Maven Central publishing
102+
103+
Publishing uses the `central-publishing-maven-plugin` v0.7.0 with `autoPublish: false`
104+
and `waitUntil: validated`. The plugin's `publishingServerId: central` maps to the
105+
`<server id="central">` entry in `settings/settings.xml`.
106+
107+
**SNAPSHOT publishing** requires:
108+
- `central-publishing-maven-plugin` v0.7.0+ (earlier versions don't support SNAPSHOTs)
109+
- Explicit enablement per namespace at central.sonatype.com → Namespaces → dropdown →
110+
"Enable SNAPSHOTs"
111+
- Without enablement, SNAPSHOT deploys return **403 Forbidden**
112+
- SNAPSHOTs are not validated, can be overwritten, and are cleaned up after ~90 days
113+
114+
## GitHub Packages publishing
115+
116+
Uses `mvn deploy:deploy-file` with explicit artifact coordinates. **Do NOT use
117+
`-DpomFile=pom.xml`** — the `deploy-file` goal reads the pom literally without resolving
118+
Maven properties. Since our pom has `<version>${revision}</version>`, it would see
119+
`${revision}` as the literal version string (invalid characters: `$`, `{`, `}`). Instead,
120+
specify coordinates directly: `-DgroupId=... -DartifactId=... -Dversion=... -Dpackaging=jar`.
121+
122+
Authentication uses a `<server id="github">` entry in `settings/settings.xml` with
123+
`${githubusername}` and `${githubtoken}` properties, resolved via `-D` flags on the
124+
command line.
125+
126+
## Azure Pipelines variable scoping (CRITICAL pitfall)
127+
128+
Azure Pipelines has two expression syntaxes with **very different scoping rules**:
129+
130+
- **`${{ variables.X }}`** — Compile-time expression. Can **only** see variables defined
131+
in the **same template file**. Variables from other templates are invisible at compile
132+
time, even if they are in the same pipeline.
133+
- **`$(X)`** — Runtime macro. Resolves **after** all variable scopes (pipeline + job) are
134+
merged. Works across template boundaries.
135+
136+
This means:
137+
- `${{ if eq(variables.isPrerelease, 'true') }}` in `job-variables.yml` **cannot** see
138+
`isPrerelease` defined in `global-variables.yml` — it will always evaluate to false.
139+
- The fix: compute dependent values (like `versionSuffix`) in the **same template** as
140+
the variables they reference, then use `$(versionSuffix)` runtime macros elsewhere.
141+
- Also note: `${{ true }}` as a variable value becomes the string `'True'` (capital T).
142+
Use the string `'true'` (lowercase) to avoid comparison mismatches.
143+
144+
### Example of the scoping pitfall
145+
146+
```yaml
147+
# global-variables.yml — defines isPrerelease
148+
variables:
149+
isPrerelease: 'true'
150+
151+
# job-variables.yml — WRONG: can't see isPrerelease from global-variables.yml
152+
variables:
153+
versionSuffix: ${{ if eq(variables.isPrerelease, 'true') }}-SNAPSHOT${{ else }}${{ endif }}
154+
# ↑ Always evaluates to empty string because isPrerelease is invisible here
155+
156+
# FIX: compute versionSuffix in global-variables.yml alongside isPrerelease,
157+
# then reference as $(versionSuffix) runtime macro in job-variables.yml
158+
```
159+
160+
## Pipeline modification checklist
161+
162+
When modifying the CI/CD pipeline:
163+
1. **Test GPG signing with `mvn deploy`**, not `mvn package` (GPG is in `verify` phase)
164+
2. **Never use `-DpomFile=pom.xml`** with `deploy:deploy-file` — use explicit coordinates
165+
3. **Keep compile-time conditionals in the same template** as the variables they reference
166+
4. **Use string `'true'`/`'false'`** for boolean pipeline variables, not `${{ true }}`
167+
5. **Verify CodeSignTool via Docker only** — never install it directly on the build agent
168+
6. **All credentials flow through `settings/settings.xml`** via Maven property placeholders
169+
resolved by `-D` flags — Azure DevOps auto-masks vault secrets in logs
170+
171+
## Full deploy command reference
172+
173+
The complete `mvn deploy` invocation for a release build (for reference):
174+
175+
```bash
176+
mvn deploy -P release \
177+
--settings settings/settings.xml \
178+
"-Drevision=$(version)" \
179+
"-Dsigningkeystorepassword=$(SigningStorePassword)" \
180+
"-Dgpgkeyname=$(gpgKeyName)" \
181+
"-Dcentralusername=$(SonatypeUserToken)" \
182+
"-Dcentralpassword=$(SonatypeRepositoryPassword)"
183+
```
184+
185+
All `-D` properties map to `<server>` entries in `settings/settings.xml` via Maven
186+
property placeholders (e.g., `<passphrase>${signingkeystorepassword}</passphrase>`).
187+
Azure DevOps automatically masks vault secrets in log output.

0 commit comments

Comments
 (0)