Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions crates/rite-ls/src/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -81,6 +85,11 @@ pub fn detect_context(line: &str, col: usize, cursor_line: u32) -> Option<Comple
typed: rest.to_string(),
});
}
if let Some(rest) = trimmed.strip_prefix("retry: ") {
return Some(CompletionContext::RetryRef {
typed: rest.to_string(),
});
}

None
}
Expand Down Expand Up @@ -115,9 +124,28 @@ pub fn completions_for(
declaration_completions(span_map.backends.keys().map(|s| s.as_str()), &typed)
}
}
CompletionContext::RetryRef { typed } => 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<CompletionItem> {
[
("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<Item = &'a str>,
typed: &str,
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions examples/showcase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions examples/showcase/retry_guards.rite.yaml
Original file line number Diff line number Diff line change
@@ -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.