OXYGENHUB

Hub: old clients are not sent tags only a certificate authorizes (refs oxygen#131) a0a1601eb5a5

oxy 0.5.0 and older refuse a release tag signed by a keyless agent's
certified key and, with it, the whole clone of a repository with a policy.
HubBackend implements oxy-server's new adjust_refs hook with the shared
rule (oxy_server::tailor_refs): a client that does not declare
certified-tags in Oxy-Features is not sent those tags. Needs the oxygen pin
with oxygen#131.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 45a6a4c1e0c5
When
· 2026-10-20 15:46 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
a0a1601eb5a532d59ffadb89cbe957a7459cca6c44707a8d60aa4e63698b43e9
Depends on
0ee5c3fb32eb 42fa95540468 a7e6e8e6094c b5668108487f b5c199bd6b1a c7067c7bda56 d23464c0ac60
Depended on by
4c77a1c8aad8
In bookmarks
api-unknown-fields diffusion-pin-bookmark draft-ready main proposal-list-fast
In tags
v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
3 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified CHANGELOG.md +1 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **Released oxy can clone repositories with an agent's release tag again (oxygen#131):** since keyless agents could tag releases with ten-minute certified keys, oxy 0.5.0 and older refused such a tag when cloning or fetching a repository with a trust policy (`key … may not create tags/v0.5.0 under this repository's policy`), and with it the whole clone, so andymccall/oxygen could not be cloned by any released client. The hub now leaves those tags out of its answer to a client that does not declare `certified-tags` in the new `Oxy-Features` request header (oxygen#131), which sees the repository as if the tag did not exist; current clients get it as before. Needs the oxygen pin with oxygen#131.
7 8
- **Repository settings are one page per setting (#278):** a repository's Settings was one long page; it is now a sidebar of grouped pages (General, Code, Flow, Integrations, Data, Danger zone) with the page shown beside it, the current one marked, each at its own address (`/{owner}/{name}/settings/push-protection`, `/settings/runs`, …; `/settings` is General). Pages a role cannot use are not listed and are not found. Every form keeps its address and now returns to its own page with what happened, so reloading does not send it again. Links elsewhere point at the new pages (the push protection refusal names `/settings/push-protection`), and a kept `/settings#section` link opens the page that has it. On a narrow screen the sidebar becomes a compact list above the page. An archived repository's settings open for its admins again, so they can bring it back.
8 9
- **Proposal lists and the proposals API are much faster on large files:** listing a repository's proposals rendered every open proposal's full diff and looked up each new line's history separately, recomputing a whole file's lines for every line; lists, the API, auto-land and the checks after a push now skip the rendering, and the proposal page computes each file's lines once.
9 10
- **Keyless agents can push release tags (andymccall/oxygen#129):** A tag signed by a key the hub certified (an agent's or a Flow job's ten-minute key) was refused "unknown key", and then refused by the policy. With oxy's fix the hub accepts it under the policy's subject grant and countersigns when it accepted it, as it does a bookmark move. It logs the tag's acceptance in the transparency log beside the tag, and serves both with the tag in every refs answer, so a client fetching the tag months later checks the certificate at the hub's time. Runners read repositories with their own oxy, so they need rebuilding to check such tags; older ones ignore the new fields.
…
257 unchanged lines
modified crates/hub/src/protocol.rs +8 −0
…
294 unchanged lines
295 295
        }
296 296
    }
297 297
298 +
    /// Tailor the refs answer to the client asking (oxygen#131): one that
299 +
    /// does not declare `certified-tags` in `Oxy-Features` (oxy 0.5.0 and
300 +
    /// older) is not sent a tag that only a certified key's certificate
301 +
    /// authorizes, which it would refuse and, with it, the whole clone.
302 +
    fn adjust_refs(&self, _name: &str, repo: &Repo, refs: &mut oxy_core::protocol::RefsResponse, headers: &HeaderMap) {
303 +
        oxy_server::tailor_refs(repo, refs, headers);
304 +
    }
305 +
298 306
    /// Push protection (hub spec §15.4, #268): a push adding what looks
299 307
    /// like a secret is refused, saying where, unless the repository turned
300 308
    /// it off or a maintainer allowed that finding's fingerprint.
…
288 unchanged lines
modified crates/hub/tests/flow.rs +14 −0
…
5885 unchanged lines
5886 5886
    a.pull("origin").unwrap();
5887 5887
    let t = a.tags().unwrap().into_iter().find(|t| t.name == "v0.2.0").expect("pulled");
5888 5888
    assert_eq!(t.signer.as_deref(), Some("claude"));
5889 +
5890 +
    // oxy 0.5.0 and older, which send no `Oxy-Features`, refuse a tag only a
5891 +
    // certificate authorizes and, with it, the whole clone (oxygen#131): the
5892 +
    // hub leaves it out of their refs answer, and Andy's own tag in.
5893 +
    a.tag_create("v0.1.0", &a.bookmark_heads().unwrap(), "by Andy").unwrap();
5894 +
    assert!(a.push("origin", false).unwrap().iter().any(|r| r.name == "tags/v0.1.0" && r.ok));
5895 +
    let bare = oxy_core::protocol::http_get(&format!("{url}/oxy/v1/refs"), &RemoteOptions::default()).unwrap();
5896 +
    let (_, old) = oxy_core::protocol::wire::decode_refs(&bare).unwrap();
5897 +
    let names: Vec<String> = old.tags.into_iter().map(|t| t.name).collect();
5898 +
    assert_eq!(names, ["v0.1.0"], "no Oxy-Features header");
5899 +
    let refs = open_remote(&url, &RemoteOptions::default()).unwrap().refs().unwrap();
5900 +
    let mut names: Vec<String> = refs.tags.into_iter().map(|t| t.name).collect();
5901 +
    names.sort();
5902 +
    assert_eq!(names, ["v0.1.0", "v0.2.0"], "a current client declares certified-tags");
5889 5903
}
5890 5904
5891 5905
/// Keyless step 5 (oxygenhub#241): Andy grants an agent with his own key;
…
200 unchanged lines