返回 CodeWhale
dsh_credentials.rs
根目录 / crates / tui / src / dsh_credentials.rs
1 //! Read-only DeepSeek Harness credential import.
2 //!
3 //! Official `dsh` stores API keys as a YAML mapping in
4 //! `$DSH_HOME/.credentials.yaml`. Codewhale may read `DEEPSEEK_API_KEY` from
5 //! that exact file only after `codewhale auth external-consent`. The file is
6 //! never written, refreshed, or loaded into the process environment.
7
8 use anyhow::{Result, bail};
9 use codewhale_config::ExternalCredentialReadGrant;
10
11 const DEEPSEEK_API_KEY_REF: &str = "DEEPSEEK_API_KEY";
12
13 /// Extract the DeepSeek API key from a granted dsh credentials document.
14 pub(crate) fn deepseek_api_key_from_grant(
15 grant: &ExternalCredentialReadGrant,
16 ) -> Result<Option<String>> {
17 if grant.source() != codewhale_config::ExternalCredentialSource::DshCli {
18 bail!(
19 "DeepSeek Harness import requires a dsh_cli grant, not {}",
20 grant.source().as_str()
21 );
22 }
23 let Some(text) = crate::external_credentials::read_to_string(grant)? else {
24 return Ok(None);
25 };
26 parse_dsh_deepseek_api_key(&text)
27 }
28
29 /// Strict subset of dsh-credentials-local: a mapping of POSIX identifiers to
30 /// non-empty strings. Nested values, empty strings, and duplicate keys fail
31 /// closed. Only `DEEPSEEK_API_KEY` is returned.
32 pub(crate) fn parse_dsh_deepseek_api_key(text: &str) -> Result<Option<String>> {
33 let mut found = None;
34 let mut seen = std::collections::BTreeSet::new();
35 for (index, raw) in text.lines().enumerate() {
36 let line = raw.trim();
37 if line.is_empty() || line.starts_with('#') {
38 continue;
39 }
40 let Some((key, value)) = line.split_once(':') else {
41 bail!(
42 "DeepSeek Harness credentials line {} is not `KEY: value`",
43 index + 1
44 );
45 };
46 let key = key.trim();
47 if !is_posix_identifier(key) {
48 bail!(
49 "DeepSeek Harness credentials line {} has a non-identifier key",
50 index + 1
51 );
52 }
53 if !seen.insert(key.to_string()) {
54 bail!("DeepSeek Harness credentials declare `{key}` more than once");
55 }
56 let value = unquote_yaml_string(value.trim()).map_err(|reason| {
57 anyhow::anyhow!(
58 "DeepSeek Harness credentials line {} is invalid: {reason}",
59 index + 1
60 )
61 })?;
62 if value.is_empty() {
63 bail!(
64 "DeepSeek Harness credentials line {} has an empty value",
65 index + 1
66 );
67 }
68 if key == DEEPSEEK_API_KEY_REF {
69 found = Some(value);
70 }
71 }
72 Ok(found)
73 }
74
75 fn is_posix_identifier(value: &str) -> bool {
76 let mut chars = value.chars();
77 matches!(chars.next(), Some('A'..='Z' | 'a'..='z' | '_'))
78 && chars.all(|ch| matches!(ch, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_'))
79 }
80
81 fn unquote_yaml_string(value: &str) -> Result<String, &'static str> {
82 if value.starts_with('{')
83 || value.starts_with('[')
84 || value.starts_with('|')
85 || value.starts_with('>')
86 {
87 return Err("nested or block YAML is not supported");
88 }
89 if let Some(inner) = value
90 .strip_prefix('"')
91 .and_then(|rest| rest.strip_suffix('"'))
92 {
93 if inner.contains('\\') {
94 return Err("escaped quoted strings are not supported");
95 }
96 return Ok(inner.to_string());
97 }
98 if let Some(inner) = value
99 .strip_prefix('\'')
100 .and_then(|rest| rest.strip_suffix('\''))
101 {
102 if inner.contains('\'') {
103 return Err("escaped single-quoted strings are not supported");
104 }
105 return Ok(inner.to_string());
106 }
107 if value.contains(':') && value.contains(' ') {
108 return Err("unquoted mapping values are not supported");
109 }
110 Ok(value.to_string())
111 }
112
113 #[cfg(test)]
114 mod tests {
115 use super::*;
116
117 #[test]
118 fn parses_plain_and_quoted_deepseek_key() {
119 assert_eq!(
120 parse_dsh_deepseek_api_key("DEEPSEEK_API_KEY: sk-live\n").unwrap(),
121 Some("sk-live".to_string())
122 );
123 assert_eq!(
124 parse_dsh_deepseek_api_key(
125 "DEEPSEEK_API_KEY: \"sk-quoted\"\nOPENAI_API_KEY: sk-other\n"
126 )
127 .unwrap(),
128 Some("sk-quoted".to_string())
129 );
130 }
131
132 #[test]
133 fn missing_deepseek_key_is_absent_not_an_error() {
134 assert_eq!(
135 parse_dsh_deepseek_api_key("OPENAI_API_KEY: sk-other\n").unwrap(),
136 None
137 );
138 }
139
140 #[test]
141 fn rejects_empty_values_duplicates_and_nested_yaml() {
142 assert!(parse_dsh_deepseek_api_key("DEEPSEEK_API_KEY:\n").is_err());
143 assert!(
144 parse_dsh_deepseek_api_key("DEEPSEEK_API_KEY: sk-a\nDEEPSEEK_API_KEY: sk-b\n").is_err()
145 );
146 assert!(parse_dsh_deepseek_api_key("DEEPSEEK_API_KEY: {nested: true}\n").is_err());
147 }
148 }
149
149 lines RUST