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 |