OXYGENHUB

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