Pin oxygen main: scoped delegates; the hub needs changes and refs b43a9ab0498f
A user's delegation to the hub key must carry the changes and refs scopes and be in force: the hub signs the change and attests the reference (oxygen#67). The recipes say --scope changes,refs; the trust page counts any scope as delegated.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-04 02:56 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
b43a9ab0498f6d46e0c815101db76773ad99e2edacce9d70612583d3e2b85919- Depends on
- 145306044896 380996db84e5 43e8ca33bc5d 8a3334b7a364 93be9ce4653e d23464c0ac60
- Depended on by
- 40a43921ef42 787632570bf9 8933faff2043 f28389bd6aab
- In bookmarks
- api-unknown-fields cancel-landed-runs clear-waiting close-proposal-140 diffusion-pin-bookmark draft-ready image-check job-waits main oh-comment-body-file proposal-list-fast run-one-job wiki-says-so
- In tags
- v0.4.0 v0.4.1 v0.4.2 v0.5.0 v0.5.1 v0.5.2 v0.6.0 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
- 16 atoms, 0 tree operations, 7 files
Browse the tree at this change
modified crates/hub/src/repos/view.rs +1 −1
| … | 1285 unchanged lines |
||
| 1286 | 1286 | let mut delegated = Vec::new(); |
|
| 1287 | 1287 | if let Some(p) = repo.current_policy()?.map(|(_, sp)| sp.policy) { |
|
| 1288 | 1288 | for i in &p.identities { |
|
| 1289 | - | if i.delegates.contains(hub_key) { |
|
| 1289 | + | if i.delegate(hub_key).is_some() { |
|
| 1290 | 1290 | delegated.push(KeyView { key: i.hash().to_hex(), short: i.hash().short(12), name: i.name.clone(), email: i.email.clone() }); |
|
| 1291 | 1291 | } |
|
| 1292 | 1292 | } |
|
| … | 85 unchanged lines |
modified crates/hub/src/web/edit.rs +9 −3
| … | 12 unchanged lines |
||
| 13 | 13 | use askama::Template; |
|
| 14 | 14 | use axum::extract::{Form, Path, Query, State}; |
|
| 15 | 15 | use axum::response::{IntoResponse, Redirect, Response}; |
|
| 16 | - | use oxy_core::identity::Identity; |
|
| 16 | + | use oxy_core::identity::{Identity, Scope}; |
|
| 17 | 17 | use oxy_core::repo::FileEdit; |
|
| 18 | 18 | use serde::Deserialize; |
|
| 19 | 19 | ||
| 20 | 20 | /// The user's linked identity that names the hub key as a delegate. |
|
| 21 | 21 | pub async fn delegated_identity(app: &Shared, account_id: i64) -> Option<Identity> { |
|
| 22 | 22 | let linked = accounts::identities_of(&app.db, account_id).await.ok()?; |
|
| 23 | - | linked.into_iter().filter_map(|l| Identity::decode(&l.object).ok()).find(|i| i.delegates.contains(&app.hub_key.key)) |
|
| 23 | + | // The hub signs the user's changes and attests the reference for them, |
|
| 24 | + | // so the delegation needs both scopes, and must still be in force. |
|
| 25 | + | let now = crate::db::now_ms() as u64; |
|
| 26 | + | linked |
|
| 27 | + | .into_iter() |
|
| 28 | + | .filter_map(|l| Identity::decode(&l.object).ok()) |
|
| 29 | + | .find(|i| i.delegates_to(&app.hub_key.key, Scope::CHANGES | Scope::REFS, now)) |
|
| 24 | 30 | } |
|
| 25 | 31 | ||
| 26 | 32 | /// Everything needed to sign for the user, or why not. |
|
| 27 | 33 | pub async fn signer_for(app: &Shared, account_id: i64) -> Result<Signer, String> { |
|
| 28 | 34 | let author = delegated_identity(app, account_id).await.ok_or_else(|| { |
|
| 29 | - | "You have not delegated to this hub's key yet. From a checkout with your identity: download the hub key from /.well-known/oxyhub-key, run `oxy identity delegate add oxyhub-key.identity`, then link the new identity under Settings › Keys (`oxy identity export`).".to_string() |
|
| 35 | + | "You have not delegated to this hub's key yet. From a checkout with your identity: download the hub key from /.well-known/oxyhub-key, run `oxy identity delegate add oxyhub-key.identity --scope changes,refs`, then link the new identity under Settings › Keys (`oxy identity export`).".to_string() |
|
| 30 | 36 | })?; |
|
| 31 | 37 | let (hub_key, hub_secret) = crate::keys::load_secret(&app.config.keys_dir()).map_err(|e| format!("the hub key is unavailable: {e}"))?; |
|
| 32 | 38 | Ok(Signer { author, hub_key, hub_secret, hub_identity: app.hub_key.identity.clone() }) |
|
| … | 237 unchanged lines |
modified crates/hub/src/web/help.rs +4 −1
| … | 40 unchanged lines |
||
| 41 | 41 | } |
|
| 42 | 42 | ||
| 43 | 43 | pub fn delegate_cmd(&self) -> String { |
|
| 44 | - | format!("curl -fsSL {}/.well-known/oxyhub-key -o hub.identity\noxy identity delegate add hub.identity", self.base) |
|
| 44 | + | format!( |
|
| 45 | + | "curl -fsSL {}/.well-known/oxyhub-key -o hub.identity\noxy identity delegate add hub.identity --scope changes,refs", |
|
| 46 | + | self.base |
|
| 47 | + | ) |
|
| 45 | 48 | } |
|
| 46 | 49 | ||
| 47 | 50 | pub fn clone_cmd(&self) -> String { |
|
| … | 7 unchanged lines |
modified crates/hub/tests/edits.rs +2 −2
| … | 156 unchanged lines |
||
| 157 | 157 | assert!(r.body.contains("not delegated"), "refused: {}", r.body); |
|
| 158 | 158 | ||
| 159 | 159 | // Delegate to the hub key (the policy follows the rotation), push it, link the new identity. |
|
| 160 | - | client.delegate_add(app.hub_key.identity.clone()).unwrap(); |
|
| 160 | + | client.delegate_add(app.hub_key.identity.clone(), oxy_core::identity::Scope::CHANGES | oxy_core::identity::Scope::REFS, None).unwrap(); |
|
| 161 | 161 | assert!(client.push("origin", false).unwrap().iter().all(|r| r.ok), "policy update pushed"); |
|
| 162 | 162 | let key2 = client.config.author_key.unwrap(); |
|
| 163 | 163 | let id2 = client.identity(&key2).unwrap(); |
|
| 164 | - | assert!(id2.delegates.contains(&app.hub_key.key)); |
|
| 164 | + | assert!(id2.delegate(&app.hub_key.key).is_some()); |
|
| 165 | 165 | db::accounts::link_identity(&app.db, andy_id, &key2.to_hex(), &id2.name, &id2.email, &id2.encode()).await.unwrap(); |
|
| 166 | 166 | ||
| 167 | 167 | // An edit lands on main, authored by Andy, signed by the hub as his delegate. |
|
| … | 311 unchanged lines |
replaced deps/oxygenflow +0 −0
No line changes.
modified docs/spec.md +1 −1
| … | 196 unchanged lines |
||
| 197 | 197 | ||
| 198 | 198 | A repository with no policy accepts any reference update from any account with `write`. The settings page says so plainly and offers to create a policy. Policy editing in the browser produces a **policy proposal**: a signed policy object that roots sign with `oxy policy sign` from their own clients; the hub collects signatures until the threshold is met, then applies the update as an ordinary push. The hub never holds a root key. |
|
| 199 | 199 | ||
| 200 | - | The hub has one identity of its own, the **hub delegate key**, generated at `init` and published at `/.well-known/oxyhub-key`. It is used only to sign changes the hub itself creates on behalf of a signed-in user (web edits, §6.9; proposal landings, §7.6). Such a change carries the user's linked key as `author` and the hub key as signer, which the author-or-delegate rule (spec §14.4) accepts only when the user's identity object lists the hub key as a delegate: the user opts in once with `oxy identity delegate add <hub key>` and re-links the rotated identity (§5.3). Policy is then checked exactly as for the user's own signature, since a rule that names the author covers the author's delegates (spec §14.5). If the user has not delegated, or the policy does not authorise them for the reference, the hub refuses the action and shows the client recipe. Delegation is per user and revocable by the user alone (`oxy identity delegate remove`); the hub never needs a policy entry of its own. |
|
| 200 | + | The hub has one identity of its own, the **hub delegate key**, generated at `init` and published at `/.well-known/oxyhub-key`. It is used only to sign changes the hub itself creates on behalf of a signed-in user (web edits, §6.9; proposal landings, §7.6). Such a change carries the user's linked key as `author` and the hub key as signer, which the author-or-delegate rule (spec §14.4) accepts only when the user's identity object lists the hub key as a delegate with the `changes` and `refs` scopes (Oxygen spec §14.2; the hub signs the change and attests the reference), still in force: the user opts in once with `oxy identity delegate add <hub key> --scope changes,refs` and re-links the rotated identity (§5.3). Policy is then checked exactly as for the user's own signature, since a rule that names the author covers the author's delegates (spec §14.5). If the user has not delegated, or the policy does not authorise them for the reference, the hub refuses the action and shows the client recipe. Delegation is per user and revocable by the user alone (`oxy identity delegate remove`); the hub never needs a policy entry of its own. |
|
| 201 | 201 | ||
| 202 | 202 | ### 5.6 Personal access tokens |
|
| 203 | 203 | ||
| … | 459 unchanged lines |
modified templates/trust.html +1 −1
| … | 36 unchanged lines |
||
| 37 | 37 | </section> |
|
| 38 | 38 | <section class="panel"> |
|
| 39 | 39 | <h2>Hub delegate key</h2> |
|
| 40 | - | <p class="small">Changes the hub makes for you in the browser (later milestones) are signed with this key on your behalf, once you have run <code>oxy identity delegate add hub.identity</code> with the file from <a href="/.well-known/oxyhub-key">/.well-known/oxyhub-key</a>.</p> |
|
| 40 | + | <p class="small">Changes the hub makes for you in the browser (later milestones) are signed with this key on your behalf, once you have run <code>oxy identity delegate add hub.identity --scope changes,refs</code> with the file from <a href="/.well-known/oxyhub-key">/.well-known/oxyhub-key</a>.</p> |
|
| 41 | 41 | <p><code class="full">{{ t.hub_key }}</code></p> |
|
| 42 | 42 | {% if t.hub_delegated_by.is_empty() %}<p class="mute small">Nobody in this repository's policy has delegated to it.</p>{% else %}<p class="small">Delegated by: {% for k in t.hub_delegated_by %}{{ k.name }}{% if !loop.last %}, {% endif %}{% endfor %}</p>{% endif %} |
|
| 43 | 43 | </section> |
|
| … | 1 unchanged lines |