Skip to content

Commit cfbcc6d

Browse files
committed
Merge origin/main into platform: adopt ADR-0011 JSON-RPC-over-NATS binding
Bring platform current with two weeks of main so the divergent branches stop drifting; the ACP-over-NATS surface now speaks the shared jsonrpc-nats wire binding. Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
2 parents 440b603 + 345a56a commit cfbcc6d

1,374 files changed

Lines changed: 149940 additions & 63852 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
name: type-safe-error-handling
3+
description: Replace string-matched error classification with typed domain errors, value objects, and explicit boundary mappings. Use when code derives behavior from error messages, substrings, magic prefixes, loosely typed status strings, denial reasons, protocol categories, validation messages, or other stringly typed logic that should be represented at the data type level.
4+
allowed-tools:
5+
- Bash
6+
- Read
7+
- Edit
8+
- Grep
9+
---
10+
11+
# Type-Safe Error Handling
12+
13+
Use this skill when code lets human-readable text carry domain meaning. The
14+
agent should learn the shape of the domain, then make the semantic decision live
15+
in a type instead of in wording.
16+
17+
## Mental Model
18+
19+
A message can describe a fact, but once behavior depends on that message the
20+
message has become an accidental API. If changing a sentence can change an auth
21+
decision, retry policy, denial category, telemetry status, or protocol response,
22+
the code is missing a type.
23+
24+
Think in terms of information flow:
25+
26+
- Where is the domain fact first known?
27+
- Where is it collapsed into a `String`, status label, magic prefix, or loosely
28+
typed field?
29+
- Which later decision is trying to reconstruct that fact?
30+
- Which existing enum, value object, source error, or wire type should carry the
31+
fact instead?
32+
33+
## Principles
34+
35+
- Name the semantic fact at the source. `UnknownAccount`, `InvalidCredentials`,
36+
`MissingCredentialMaterial`, or `VerifierUnavailable` are better decision
37+
inputs than messages containing those words.
38+
- Preserve meaning across layers. A boundary conversion should be
39+
variant-to-variant or field-to-field, not format-and-parse.
40+
- Keep display text at presentation edges. Logs, diagnostics, and user-facing
41+
messages may stay stable, but they should explain typed decisions rather than
42+
drive them.
43+
- Preserve source chains when they explain cause. Do not choose between correct
44+
classification and observability if the error model can represent both.
45+
- Let wire compatibility and domain modeling coexist. Keep external strings,
46+
codes, or protobuf fields stable when required, but convert to richer domain
47+
types inside the owning package.
48+
- Respect local architecture. In Rust, keep value objects inside the owning
49+
crate unless ADR 0002 justifies a package boundary. For wire and persistence
50+
contracts, follow ADR 0009.
51+
52+
## Key Examples
53+
54+
Typed boundary mapping:
55+
56+
```rust
57+
impl From<ApiKeyError> for AuthCalloutError {
58+
fn from(error: ApiKeyError) -> Self {
59+
match error {
60+
ApiKeyError::Empty => CredentialError::InvalidRequest(...).into(),
61+
ApiKeyError::Unknown => CredentialError::InvalidCredentials(...).into(),
62+
ApiKeyError::AudienceMismatch { .. } => CredentialError::InvalidRequest(...).into(),
63+
ApiKeyError::CallerIdDerivation(_) => CredentialError::InvalidCredentials(...).into(),
64+
}
65+
}
66+
}
67+
```
68+
69+
String-derived behavior:
70+
71+
```rust
72+
let category = if error.to_string().contains("not found") {
73+
DenialCategory::InvalidCredentials
74+
} else {
75+
DenialCategory::InternalError
76+
};
77+
```
78+
79+
The first shape carries meaning through variants. The second makes wording part
80+
of control flow.
81+
82+
Local examples to learn from:
83+
84+
- `rsworkspace/crates/a2a-auth-callout/src/denial_category.rs` classifies
85+
`CredentialVerification(String)` with `msg.contains(...)`. The category
86+
decision belongs in typed credential or domain errors that map directly to
87+
`DenialCategory`.
88+
- `rsworkspace/crates/a2a-auth-callout/src/account_resolver.rs` converts
89+
`AccountResolverError` through `value.to_string()`. The resolver already knows
90+
whether the problem is `EmptyRequest` or `Unknown`; that distinction should
91+
survive until category mapping.
92+
- `ApiKeyError`-style mappings should stay variant-to-variant. Empty input,
93+
unknown key, caller-id derivation failure, and audience mismatch are different
94+
facts even if their display messages all end up near credential verification.
95+
96+
## Smells
97+
98+
Pause and reason from the principles when you see:
99+
100+
- `contains`, `starts_with`, regexes, or equality checks against error messages.
101+
- `map_err(|e| SomeError(e.to_string()))` before another layer classifies the
102+
result.
103+
- catch-all `String` variants used for multiple semantic cases.
104+
- tests proving semantic behavior through substrings.
105+
- protocol categories inferred from local wording instead of typed tags.
106+
107+
## Verification
108+
109+
Confidence comes from tests that prove the type-level contract:
110+
111+
- each source variant maps to the intended typed category.
112+
- wire-visible strings or codes remain compatible when compatibility matters.
113+
- wrapped sources remain available through `source()` where supported.
114+
- unknown or internal errors do not accidentally become user-correctable errors.
115+
116+
String assertions are allowed for stable display text, but not as the only proof
117+
of semantic behavior.
118+
119+
## Done
120+
121+
The task is done when a future wording change cannot change behavior, and a
122+
reviewer can point to the type that owns each semantic decision.

.config/mise/tasks/rust-pr-check

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,32 @@ step() {
4646
# 1. fmt --check — mirrors CI 'Check formatting'.
4747
step "fmt-check" cargo fmt --all -- --check
4848

49-
# 2. clippy from a clean cache — workspace lints (deny warnings, unwrap_used,
49+
# 2. Repo-owned policy lints.
50+
mapfile -t DYLINT_TOOLCHAIN_CONFIG < <(
51+
TOOLCHAIN_FILE="dylints/trogon_lints/rust-toolchain.toml" python -c 'import os, sys, tomllib; toolchain = tomllib.load(open(os.environ["TOOLCHAIN_FILE"], "rb"))["toolchain"]; sys.stdout.write("\n".join([toolchain["channel"], *toolchain.get("components", [])]) + "\n")'
52+
)
53+
DYLINT_TOOLCHAIN="${DYLINT_TOOLCHAIN_CONFIG[0]}"
54+
DYLINT_COMPONENTS=("${DYLINT_TOOLCHAIN_CONFIG[@]:1}")
55+
56+
step "repo-rust-policy-lint-toolchain" rustup toolchain install "$DYLINT_TOOLCHAIN" --profile minimal
57+
if [[ ${#DYLINT_COMPONENTS[@]} -gt 0 ]]; then
58+
step "repo-rust-policy-lint-components" rustup component add "${DYLINT_COMPONENTS[@]}" --toolchain "$DYLINT_TOOLCHAIN"
59+
fi
60+
step "repo-rust-policy-lint-tests" bash -c 'cd dylints/trogon_lints && rustup run "$1" cargo test' _ "$DYLINT_TOOLCHAIN"
61+
step "repo-rust-policy-lints" env -u RUSTUP_TOOLCHAIN DYLINT_RUSTFLAGS='-Derror-string-comparison' rustup run "$DYLINT_TOOLCHAIN" cargo dylint --path dylints/trogon_lints --workspace --no-deps -- --all-features
62+
63+
# 3. clippy from a clean cache — workspace lints (deny warnings, unwrap_used,
5064
# expect_used, panic) only fire on a real rebuild; cached builds skip them.
5165
step "clean-crate" cargo clean -p "$CRATE"
5266
step "clippy" cargo clippy -p "$CRATE" --all-targets --all-features
5367

54-
# 3. Unit tests.
68+
# 4. Unit tests.
5569
step "test" cargo test -p "$CRATE" --all-targets
5670

57-
# 4. doc tests — CI runs these too.
71+
# 5. doc tests — CI runs these too.
5872
step "doc-test" cargo test -p "$CRATE" --doc
5973

60-
# 5. Coverage gate — delegate to rust-coverage-check which runs cargo cov +
74+
# 6. Coverage gate — delegate to rust-coverage-check which runs cargo cov +
6175
# pycobertura diff against the main baseline (same comparison CI uses for
6276
# the "new uncovered statements" failure mode).
6377
cd "$ROOT"
Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: Setup Rust
2-
description: Install the mise-pinned Rust toolchain and optional rustup components
2+
description: Install the mise-pinned Rust toolchain and required rustup components
33

44
inputs:
5-
components:
6-
description: Comma-separated rustup components to ensure are installed (e.g. llvm-tools-preview)
5+
dylint-toolchain-dir:
6+
description: Directory containing the rust-toolchain.toml for repo Dylint rules
77
required: false
88
default: ''
99

@@ -12,8 +12,46 @@ runs:
1212
steps:
1313
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
1414

15-
- if: inputs.components != ''
15+
- name: Install Rust components
1616
shell: bash
17-
env:
18-
COMPONENTS: ${{ inputs.components }}
19-
run: rustup component add ${COMPONENTS//,/ }
17+
run: |
18+
rustup component add rustfmt clippy llvm-tools-preview \
19+
--toolchain "${RUSTUP_TOOLCHAIN:-$(rustup show active-toolchain | awk '{print $1}')}"
20+
21+
# Added serially here rather than through rust-toolchain.toml `targets`: that field makes
22+
# every concurrent `cargo install` during `mise install` trigger its own rustup target
23+
# download, and the parallel downloads corrupt each other (rename fails with os error 2).
24+
- name: Install wasm32 target
25+
shell: bash
26+
run: |
27+
rustup target add wasm32-unknown-unknown \
28+
--toolchain "${RUSTUP_TOOLCHAIN:-$(rustup show active-toolchain | awk '{print $1}')}"
29+
30+
- name: Install Dylint Rust components
31+
shell: bash
32+
run: |
33+
dylint_toolchain_input="${{ inputs.dylint-toolchain-dir }}"
34+
if [[ -n "$dylint_toolchain_input" ]]; then
35+
if [[ "$dylint_toolchain_input" = /* ]]; then
36+
dylint_toolchain_dir="$dylint_toolchain_input"
37+
else
38+
dylint_toolchain_dir="${GITHUB_WORKSPACE:-$PWD}/$dylint_toolchain_input"
39+
fi
40+
41+
if [[ ! -f "$dylint_toolchain_dir/rust-toolchain.toml" ]]; then
42+
echo "::error::Dylint rust-toolchain.toml not found in $dylint_toolchain_dir"
43+
exit 1
44+
fi
45+
46+
mapfile -t dylint_toolchain_config < <(
47+
TOOLCHAIN_FILE="$dylint_toolchain_dir/rust-toolchain.toml" python -c 'import os, sys, tomllib; toolchain = tomllib.load(open(os.environ["TOOLCHAIN_FILE"], "rb"))["toolchain"]; sys.stdout.write("\n".join([toolchain["channel"], *toolchain.get("components", [])]) + "\n")'
48+
)
49+
50+
dylint_toolchain="${dylint_toolchain_config[0]}"
51+
dylint_components=("${dylint_toolchain_config[@]:1}")
52+
53+
rustup toolchain install "$dylint_toolchain" --profile minimal
54+
if [[ ${#dylint_components[@]} -gt 0 ]]; then
55+
rustup component add "${dylint_components[@]}" --toolchain "$dylint_toolchain"
56+
fi
57+
fi

.github/workflows/canary-container-images.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
service:
2626
- trogon-gateway
2727
steps:
28-
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
28+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
2929
with:
3030
persist-credentials: false
3131

@@ -56,7 +56,7 @@ jobs:
5656

5757
- name: Build and push canary image
5858
id: build_image
59-
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
59+
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
6060
with:
6161
context: ${{ steps.service_config.outputs.context }}
6262
file: ${{ steps.service_config.outputs.dockerfile }}

0 commit comments

Comments
 (0)