oxy doctor checks the trust policy ad2496f7e608
A repository without one was 'Everything looks right' while every fetch warned that nothing was verified. Now a trust line: no policy is a warning naming who sets one and how; a policy shows its serial, roots, rules and expiry, warning within 30 days and failing once expired. Closes #127
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 2c2ee26d69a7
- When
- · 2026-10-19 14:26 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
ad2496f7e608641053085dd305d3a7e821d7aaebf4ca47f5a4b170837437af85- Depends on
- 387335ff7f4d 3e5772ccf9da 982864068a59 c863c7a7a2d1 ee06a6c557b7 ff0af1ee299d
- Depended on by
- nothing yet
- In bookmarks
- doctor-trust identical-sides keyless-tags main push-notes subrepo-pin-state
- In tags
- v0.10.0 v0.11.0 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 8 atoms, 0 tree operations, 4 files
Browse the tree at this change
modified crates/oxy-core/src/repo/doctor.rs +64 −0
| … | 61 unchanged lines |
||
| 62 | 62 | self.doctor_identity(&mut rep); |
|
| 63 | 63 | self.doctor_author(&mut rep); |
|
| 64 | 64 | self.doctor_portability(&mut rep); |
|
| 65 | + | self.doctor_trust(&mut rep); |
|
| 65 | 66 | for name in self.config.remotes.keys() { |
|
| 66 | 67 | self.doctor_remote(&mut rep, name, online); |
|
| 67 | 68 | } |
|
| … | 178 unchanged lines |
||
| 246 | 247 | } |
|
| 247 | 248 | } |
|
| 248 | 249 | ||
| 250 | + | /// The trust policy (spec §14.5): without one, nothing fetched is |
|
| 251 | + | /// verified, which fetch says each time; doctor says it too, with who |
|
| 252 | + | /// fixes it (oxygen#127). |
|
| 253 | + | fn doctor_trust(&self, rep: &mut DoctorReport) { |
|
| 254 | + | let policy = match self.current_policy() { |
|
| 255 | + | Ok(p) => p, |
|
| 256 | + | Err(e) => { |
|
| 257 | + | rep.push( |
|
| 258 | + | "trust", |
|
| 259 | + | Health::Fail, |
|
| 260 | + | format!("the trust policy does not load: {e}"), |
|
| 261 | + | Some("run `oxy fsck` for details".into()), |
|
| 262 | + | ); |
|
| 263 | + | return; |
|
| 264 | + | } |
|
| 265 | + | }; |
|
| 266 | + | let Some((r, sp)) = policy else { |
|
| 267 | + | rep.push( |
|
| 268 | + | "trust", |
|
| 269 | + | Health::Warn, |
|
| 270 | + | "no trust policy: changes and bookmark moves fetched here are not verified", |
|
| 271 | + | Some( |
|
| 272 | + | "the repository's owner sets one: `oxy policy init`, then `oxy policy grant FILE` for each \ |
|
| 273 | + | contributor and `oxy policy grant-subject --issuer HUB_KEY agent:NAME@ACCOUNT` for keyless \ |
|
| 274 | + | agents, and `oxy push`" |
|
| 275 | + | .into(), |
|
| 276 | + | ), |
|
| 277 | + | ); |
|
| 278 | + | return; |
|
| 279 | + | }; |
|
| 280 | + | let p = &sp.policy; |
|
| 281 | + | let now = Hlc::now().wall_ms; |
|
| 282 | + | let what = format!( |
|
| 283 | + | "policy #{} serial {}: {} root{}, {} rule{}", |
|
| 284 | + | &r.to_hex()[..12], |
|
| 285 | + | p.serial, |
|
| 286 | + | p.roots.len(), |
|
| 287 | + | if p.roots.len() == 1 { "" } else { "s" }, |
|
| 288 | + | p.rules.len(), |
|
| 289 | + | if p.rules.len() == 1 { "" } else { "s" }, |
|
| 290 | + | ); |
|
| 291 | + | let renew = Some( |
|
| 292 | + | "a root of the policy publishes a new serial (any `oxy policy` change does)".into(), |
|
| 293 | + | ); |
|
| 294 | + | if p.is_expired(now) { |
|
| 295 | + | rep.push("trust", Health::Fail, format!("{what}, expired"), renew); |
|
| 296 | + | } else if p.expires - now < 30 * 86_400_000 { |
|
| 297 | + | rep.push( |
|
| 298 | + | "trust", |
|
| 299 | + | Health::Warn, |
|
| 300 | + | format!("{what}, expires in {} days", (p.expires - now) / 86_400_000), |
|
| 301 | + | renew, |
|
| 302 | + | ); |
|
| 303 | + | } else { |
|
| 304 | + | rep.push( |
|
| 305 | + | "trust", |
|
| 306 | + | Health::Ok, |
|
| 307 | + | format!("{what}, expires in {} days", (p.expires - now) / 86_400_000), |
|
| 308 | + | None, |
|
| 309 | + | ); |
|
| 310 | + | } |
|
| 311 | + | } |
|
| 312 | + | ||
| 249 | 313 | fn doctor_remote(&self, rep: &mut DoctorReport, name: &str, online: bool) { |
|
| 250 | 314 | let area = format!("remote {name}"); |
|
| 251 | 315 | let url = &self.config.remotes[name]; |
|
| … | 73 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +13 −0
| … | 4249 unchanged lines |
||
| 4250 | 4250 | .unwrap() |
|
| 4251 | 4251 | .contains("oxy identity create")); |
|
| 4252 | 4252 | assert!(find(&r, "repository", Health::Ok).is_some()); |
|
| 4253 | + | // oxygen#127: no policy is said, with who sets one. |
|
| 4254 | + | assert!(find(&r, "trust", Health::Warn) |
|
| 4255 | + | .unwrap() |
|
| 4256 | + | .hint |
|
| 4257 | + | .unwrap() |
|
| 4258 | + | .contains("oxy policy init")); |
|
| 4253 | 4259 | ||
| 4254 | 4260 | repo.create_identity("Ada", "[email protected]", IdentityScope::Repo) |
|
| 4255 | 4261 | .unwrap(); |
|
| 4262 | + | repo.policy_init().unwrap(); |
|
| 4263 | + | let r = repo.doctor(false); |
|
| 4264 | + | let trust = find(&r, "trust", Health::Ok).expect("a fresh policy is fine"); |
|
| 4265 | + | assert!( |
|
| 4266 | + | trust.message.contains("serial 1") && trust.message.contains("1 root"), |
|
| 4267 | + | "{trust:?}" |
|
| 4268 | + | ); |
|
| 4256 | 4269 | repo.config.set("author.name", "Ada").unwrap(); |
|
| 4257 | 4270 | repo.config.set("author.email", "[email protected]").unwrap(); |
|
| 4258 | 4271 | repo.config |
|
| … | 269 unchanged lines |
modified docs/guide.md +1 −1
| … | 400 unchanged lines |
||
| 401 | 401 | oxy gc # pack loose objects, prune unreachable ones past the grace period |
|
| 402 | 402 | oxy gc --now # ignore the grace period (only when nothing else is writing) |
|
| 403 | 403 | oxy fsck # check every object, the op log, references, signatures, checkpoints |
|
| 404 | - | oxy doctor # check identity, config and remotes, with what to do about each problem |
|
| 404 | + | oxy doctor # check identity, config, trust policy and remotes, with what to do about each problem |
|
| 405 | 405 | oxy checkpoint # serialise the bookmark's state so large histories load instantly |
|
| 406 | 406 | ``` |
|
| 407 | 407 | ||
| … | 24 unchanged lines |
modified docs/spec.md +1 −1
| … | 798 unchanged lines |
||
| 799 | 799 | ||
| 800 | 800 | ### 21.2 Doctor |
|
| 801 | 801 | ||
| 802 | - | `oxy doctor` checks what commonly stands between a user and a working repository and says, for each problem, what to do next: the operation log opens and no lock was left by a dead process; the signing identity exists, its secret key file is present, and it has not expired (a warning within 30 days); the author name and email are not the defaults; and for each remote, that plain `http://` is only used on loopback, a token is set, the CA file exists, and (unless `--offline`) the remote answers and is this repository or an unbound one. Failures of remote requests anywhere in `oxy` carry the same next step (an untrusted certificate, a rejected or insufficient token, no repository at the URL, nothing listening, an unknown host, a timeout). The exit status is 1 when a check fails; `--json` gives the checks. |
|
| 802 | + | `oxy doctor` checks what commonly stands between a user and a working repository and says, for each problem, what to do next: the operation log opens and no lock was left by a dead process; the signing identity exists, its secret key file is present, and it has not expired (a warning within 30 days); the author name and email are not the defaults; the trust policy (§14.5) loads and has not expired (a warning within 30 days), and its absence is a warning, since nothing fetched is then verified, with the steps by which the repository's owner sets one (oxygen#127); and for each remote, that plain `http://` is only used on loopback, a token is set, the CA file exists, and (unless `--offline`) the remote answers and is this repository or an unbound one. Failures of remote requests anywhere in `oxy` carry the same next step (an untrusted certificate, a rejected or insufficient token, no repository at the URL, nothing listening, an unknown host, a timeout). The exit status is 1 when a check fails; `--json` gives the checks. |
|
| 803 | 803 | ||
| 804 | 804 | ## 22. Client profiles and retro hardware |
|
| 805 | 805 | ||
| … | 199 unchanged lines |