diff --git a/crates/rite-ls/src/complete.rs b/crates/rite-ls/src/complete.rs index 0fb43dc..a2df295 100644 --- a/crates/rite-ls/src/complete.rs +++ b/crates/rite-ls/src/complete.rs @@ -4,6 +4,7 @@ //! - `action:` field value completions (ActionType variants) //! - `${...}` expression completions (roles, params, materials from SpanMap) //! - `section:`, `role:`, `act:`, `backend:` field value completions from declarations +//! - `retry:` field value completions (the `never` keyword and the `{ attempts: N }` form) use crate::actions; use rite_model::Ceremony; @@ -35,6 +36,9 @@ pub enum CompletionContext { ActRef { typed: String }, /// Cursor is after `backend:`; offer declared backend names. BackendRef { typed: String }, + /// Cursor is after `retry:`; offer the `never` keyword and the + /// `{ attempts: N }` form. + RetryRef { typed: String }, } /// Detect what completion context applies on the given line. @@ -81,6 +85,11 @@ pub fn detect_context(line: &str, col: usize, cursor_line: u32) -> Option retry_completions(&typed), } } +/// The two valid `retry:` values: the bare `never` keyword and the inline +/// `{ attempts: N }` mapping. Static, since the set is fixed by the schema. +fn retry_completions(typed: &str) -> Vec { + [ + ("never", "Forbid retries on this step"), + ("{ attempts: 3 }", "Cap retries at N total attempts"), + ] + .into_iter() + .filter(|(value, _)| value.starts_with(typed)) + .map(|(value, detail)| CompletionItem { + label: value.to_string(), + detail: Some(detail.to_string()), + kind: Some(CompletionItemKind::VALUE), + ..Default::default() + }) + .collect() +} + fn declaration_completions<'a>( candidates: impl Iterator, typed: &str, @@ -292,6 +320,36 @@ mod tests { assert!(detect_context(" name: foo", 11, 0).is_none()); } + #[test] + fn detect_retry_context() { + match detect_context(" retry: ", 11, 0) { + Some(CompletionContext::RetryRef { typed }) => assert_eq!(typed, ""), + _ => panic!("expected RetryRef context"), + } + } + + #[test] + fn retry_offers_both_forms_when_empty() { + let items = completions_for( + CompletionContext::RetryRef { + typed: String::new(), + }, + &SpanMap::default(), + None, + ); + assert_eq!(sorted_labels(&items), ["never", "{ attempts: 3 }"]); + } + + #[test] + fn retry_filters_by_typed_prefix() { + let items = completions_for( + CompletionContext::RetryRef { typed: "ne".into() }, + &SpanMap::default(), + None, + ); + assert_eq!(sorted_labels(&items), ["never"]); + } + #[test] fn detect_none_when_cursor_before_dollar() { // Cursor at col 5, before the `${` which starts at col 8. diff --git a/examples/showcase/README.md b/examples/showcase/README.md index 9c6038d..5986a65 100644 --- a/examples/showcase/README.md +++ b/examples/showcase/README.md @@ -26,6 +26,15 @@ digital materials, and long structured step instructions. It generates a backup-wrapping key, escrows it under the bundled test key, and hands sealed media to a custodian. Use it to see how a dense, formal script renders across pages. +### `retry_guards.rite.yaml` — Signing Key Ceremony with Retry Guards + +A compact signing-key ceremony that demonstrates the `retry:` field. The device +steps carry a per-step retry policy: `generate_keypair` caps retries with +`retry: { attempts: 3 }`, certificate issuance forbids them with `retry: never`, +and the CSR step omits the field to show the prompt-on-transient-failure default. +It runs end to end on OpenSSL but reads as a template you could retarget to a +hardware backend. See `docs/error-handling.md` for the retry model. + ### `dice.rite.yaml` — Dice Entropy Ceremony Demonstrates verifiable ceremony randomness: a participant folds a physical dice diff --git a/examples/showcase/retry_guards.rite.yaml b/examples/showcase/retry_guards.rite.yaml new file mode 100644 index 0000000..d97c95f --- /dev/null +++ b/examples/showcase/retry_guards.rite.yaml @@ -0,0 +1,62 @@ +version: "0.2" +name: "Signing Key Ceremony with Retry Guards" +description: | + Generate an offline signing key and issue its self-signed certificate, with a + per-step retry policy on the device-touching steps. + + Demonstrates the retry: field. + +backends: + openssl: + provider: openssl + +roles: + crypto_officer: + person: "Alice Rivera" + +sections: + keygen: + name: "Key Generation and Certification" + role: ${role.crypto_officer} + steps: + generate_signing_key: + action: generate_keypair + backend: openssl + retry: + attempts: 3 + with: + algorithm: ECDSA-P256 + creates: signing_keypair + description: | + Generate the signing keypair. On a hardware backend this can fail + transiently (device busy, loose cable, token not seated). + retry: { attempts: 3 } lets the operator fix the condition and retry up + to three attempts; exhausting the cap fails the ceremony. + generate_signing_csr: + action: generate_csr + backend: openssl + reads: + signing_key: ${artifact.signing_keypair} + with: + subject: "CN=Example Signing Key,O=Example Org" + creates: signing_csr + description: | + Build the certificate signing request. No retry: field, so the default + applies: a transient failure prompts the operator to retry or abort, + with no limit. + issue_signing_cert: + action: issue_certificate + backend: openssl + retry: never + reads: + signing_key: ${artifact.signing_keypair} + csr: ${artifact.signing_csr} + with: + profile: root_ca + validity_days: 1825 + creates: signing_cert + description: | + Issue the self-signed certificate. Using the signing key is the + decisive act, and on a hardware backend each key use is + security-relevant. retry: never halts the ceremony for human judgment + instead of re-using the key automatically.