OXYGENHUB

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