caboodle/
skill_profile.rs1pub mod query;
9
10use anyhow::{bail, Result};
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13
14#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
16#[serde(rename_all = "lowercase")]
17pub enum Harness {
18 Claude,
19 Codex,
20}
21
22#[derive(Clone, Debug, Deserialize, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct Pin {
26 pub skill: String,
27 pub share_id: String,
28 pub version: String,
29}
30
31#[derive(Clone, Debug, Deserialize, Serialize)]
33#[serde(deny_unknown_fields)]
34pub struct SkillProfile {
35 pub id: String,
36 pub public_only: bool,
37 pub harnesses: Vec<Harness>,
38 pub pins: Vec<Pin>,
39}
40
41#[derive(Clone, Debug, Deserialize, Serialize)]
43#[serde(deny_unknown_fields)]
44pub struct Release {
45 pub pin: Pin,
46 pub name: String,
47 pub render_spec: String,
48 pub public: bool,
49 pub harnesses: Vec<Harness>,
50}
51
52#[derive(Clone, Debug, Deserialize, Serialize)]
54#[serde(deny_unknown_fields)]
55pub struct CatalogueSnapshot {
56 pub transaction: u64,
57 pub complete: bool,
58 pub releases: Vec<Release>,
59}
60
61#[derive(Clone, Debug, Serialize)]
63pub struct Selection {
64 pub profile: String,
65 pub transaction: u64,
66 pub harnesses: Vec<Harness>,
67 pub releases: Vec<Release>,
68}
69
70fn text(value: &str) -> bool {
71 !value.is_empty()
72 && value.len() <= 2_048
73 && !value.chars().any(|c| c.is_control() || c.is_whitespace())
74}
75
76fn valid_pin(pin: &Pin) -> bool {
77 text(&pin.skill)
78 && text(&pin.version)
79 && pin.share_id.strip_prefix("sha256:").is_some_and(|digest| {
80 digest.len() == 64
81 && digest
82 .bytes()
83 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
84 })
85}
86
87fn harnesses(input: &[Harness]) -> Result<BTreeSet<Harness>> {
88 let set: BTreeSet<_> = input.iter().copied().collect();
89 if set.is_empty() || set.len() != input.len() {
90 bail!("empty or duplicate harness selection");
91 }
92 Ok(set)
93}
94
95pub fn select(profile: &SkillProfile, snapshot: &CatalogueSnapshot) -> Result<Selection> {
102 if !text(&profile.id) || !snapshot.complete || snapshot.transaction == 0 {
103 bail!("profile or transaction-pinned catalogue is unavailable");
104 }
105 if profile.pins.is_empty() || profile.pins.len() > 128 || snapshot.releases.len() > 1_024 {
106 bail!("skill selection exceeds its bounded contract");
107 }
108 let requested = harnesses(&profile.harnesses)?;
109 let mut catalogue = BTreeMap::new();
110 for release in &snapshot.releases {
111 if !valid_pin(&release.pin)
112 || !text(&release.render_spec)
113 || release.name.is_empty()
114 || release.name.len() > 64
115 || !release
116 .name
117 .bytes()
118 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
119 {
120 bail!("invalid skill release metadata");
121 }
122 harnesses(&release.harnesses)?;
123 let key = (&release.pin.skill, &release.pin.share_id);
124 if catalogue.insert(key, release).is_some() {
125 bail!("ambiguous skill release metadata");
126 }
127 }
128 let mut selected = BTreeMap::new();
129 let mut names = BTreeSet::new();
130 for pin in &profile.pins {
131 if !valid_pin(pin) {
132 bail!("invalid profile pin");
133 }
134 let release = catalogue
135 .get(&(&pin.skill, &pin.share_id))
136 .ok_or_else(|| anyhow::anyhow!("pinned skill release is unavailable"))?;
137 if pin.version != release.pin.version
138 || (profile.public_only && !release.public)
139 || !requested.is_subset(&harnesses(&release.harnesses)?)
140 {
141 bail!("pinned release does not satisfy the profile");
142 }
143 if selected
144 .insert(pin.skill.clone(), (**release).clone())
145 .is_some()
146 || !names.insert(release.name.clone())
147 {
148 bail!("duplicate skill identity or destination name");
149 }
150 }
151 Ok(Selection {
152 profile: profile.id.clone(),
153 transaction: snapshot.transaction,
154 harnesses: requested.into_iter().collect(),
155 releases: selected.into_values().collect(),
156 })
157}
158
159#[cfg(test)]
160mod tests;