Fall back from a checkout's own token only when the hub does not know it (401), never when it refuses it for permission (refs #155) 0588de69a84d
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 912fd3b85660
- When
- · 2026-10-28 22:30 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
0588de69a84d807a47d1013e20ecfaffe92fa1016b3470d10807e130edbcfb94- Depends on
- 2b5ee371060e 569802ebae0e 5b9c6ad45f6e 642da2636de2 68a924846379 750251b21a96 75167672ebcd 8fd78eb52ad2 982864068a59 e4595038502a ff1f62fee654
- Depended on by
- 6c3dbe2018e0 a42f9fb47e37
- In bookmarks
- main
- In tags
- v0.10.0 v0.11.0 v0.9.0
- Edits
- 12 atoms, 0 tree operations, 4 files
Browse the tree at this change
modified CHANGELOG.md +1 −1
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | - | - **Fixed: an expired checkout token no longer hides your sign-in (#155).** A checkout's own token for a remote (`remote.NAME.token`) is used first, so when it expired every fetch, pull and push answered 401 although `oxy auth login` had a good sign-in for the same hub. A refused own token now falls back to that sign-in, with a warning naming `oxy config --unset remote.NAME.token` to drop the stale one; never to another checkout's token. When both are refused, the error says to sign in again and unset it. |
|
| 5 | + | - **Fixed: an expired checkout token no longer hides your sign-in (#155).** A checkout's own token for a remote (`remote.NAME.token`) is used first, so when it expired every fetch, pull and push answered 401 although `oxy auth login` had a good sign-in for the same hub. A checkout token the hub no longer knows (401) now falls back to that sign-in, with a warning naming `oxy config --unset remote.NAME.token` to drop the stale one; never to another checkout's token, and not when the token is refused for permission (403, 404), so a narrowly scoped token is never swapped for a wider sign-in. When both are refused, the error says to sign in again and unset it. |
|
| 6 | 6 | - **Added: artifacts (Cylinder, step C1, #108).** `oxy artifact pack DIR --kind archive|package|container|runner-image|report [--arch --os --entrypoint --env K=V --label K=V]` packs a directory into an artifact: each file an ordinary blob of the repository's store, so files and chunks it holds already cost nothing, under a tree and an artifact encoding in canonical CBOR, named by the BLAKE3 of that encoding (docs/cylinder.md §4.1). `oxy artifact list | show NAME | unpack NAME DEST | verify NAME` (any unique prefix of six characters or more; `--json` on list, show and pack), and `oxy gc` keeps every artifact's blobs. The new `cylinder` crate holds the model. |
|
| 7 | 7 | - **Added: oxy on Haiku (#154).** oxy builds and runs on Haiku R1/beta6 (x86_64) with HaikuPorts' Rust; `oxy-VERSION-haiku-x86_64.tar.gz` is on the releases page from 0.8.0, with its own `SHA256SUMS-haiku`. The README says how to install it and how to build it (`ZSTD_SYS_USE_PKG_CONFIG=1`). |
|
| 8 | 8 | ||
| … | 299 unchanged lines |
modified crates/oxy-core/src/protocol.rs +12 −1
| … | 318 unchanged lines |
||
| 319 | 319 | /// `token` is the remote's own (oxygen#155), or those of enclosing |
|
| 320 | 320 | /// checkouts when it came from the credential store (oxygen#112). |
|
| 321 | 321 | pub fallback_tokens: Vec<String>, |
|
| 322 | + | /// `token` is the remote's own: fall back only when the hub says it is |
|
| 323 | + | /// not a token at all (HTTP 401: expired or unknown), never when it is |
|
| 324 | + | /// one that may not do this (403, 404), so a narrowly scoped token is |
|
| 325 | + | /// not quietly swapped for a wider sign-in (oxygen#155). |
|
| 326 | + | pub fallback_on_401_only: bool, |
|
| 322 | 327 | } |
|
| 323 | 328 | ||
| 324 | 329 | /// Opens a store for a URL it understands, or passes (`None`). Stores that |
|
| … | 65 unchanged lines |
||
| 390 | 395 | /// The token in use, and those still to try when it is refused. |
|
| 391 | 396 | token: Mutex<Option<String>>, |
|
| 392 | 397 | fallbacks: Mutex<Vec<String>>, |
|
| 398 | + | fallback_on_401_only: bool, |
|
| 393 | 399 | repo_id: Option<[u8; 16]>, |
|
| 394 | 400 | agent: ureq::Agent, |
|
| 395 | 401 | } |
|
| … | 41 unchanged lines |
||
| 437 | 443 | base: base.trim_end_matches('/').to_string(), |
|
| 438 | 444 | token: Mutex::new(options.token.clone()), |
|
| 439 | 445 | fallbacks: Mutex::new(options.fallback_tokens.iter().rev().cloned().collect()), |
|
| 446 | + | fallback_on_401_only: options.fallback_on_401_only, |
|
| 440 | 447 | repo_id: options.repo_id, |
|
| 441 | 448 | agent, |
|
| 442 | 449 | }) |
|
| … | 5 unchanged lines |
||
| 448 | 455 | loop { |
|
| 449 | 456 | let token = self.token.lock().unwrap_or_else(|p| p.into_inner()).clone(); |
|
| 450 | 457 | match self.call_once(post, endpoint, body, token.as_deref()) { |
|
| 451 | - | Err(RepoError::Msg(m)) if token.is_some() && refused(&m) => { |
|
| 458 | + | Err(RepoError::Msg(m)) |
|
| 459 | + | if token.is_some() |
|
| 460 | + | && refused(&m) |
|
| 461 | + | && (!self.fallback_on_401_only || m.contains(": HTTP 401")) => |
|
| 462 | + | { |
|
| 452 | 463 | let next = self |
|
| 453 | 464 | .fallbacks |
|
| 454 | 465 | .lock() |
|
| … | 1343 unchanged lines |
modified crates/oxy-core/src/repo/sync.rs +4 −3
| … | 1412 unchanged lines |
||
| 1413 | 1413 | let env = std::env::var("OXY_TOKEN").is_ok_and(|t| !t.is_empty()); |
|
| 1414 | 1414 | let explicit = own || env; |
|
| 1415 | 1415 | let stored = self.credentials().options_for(url); |
|
| 1416 | - | // A checkout's own token refused (expired, say) falls back to this |
|
| 1417 | - | // computer's sign-in for the hub, never to another checkout's |
|
| 1418 | - | // (oxygen#155). |
|
| 1416 | + | // A checkout's own token refused as expired or unknown (401) falls |
|
| 1417 | + | // back to this computer's sign-in for the hub, never to another |
|
| 1418 | + | // checkout's, and never when it is refused for permission (#155). |
|
| 1419 | 1419 | if own && !env { |
|
| 1420 | 1420 | if let Some(t) = stored |
|
| 1421 | 1421 | .token |
|
| … | 1 unchanged lines |
||
| 1423 | 1423 | .filter(|t| options.token.as_ref() != Some(t)) |
|
| 1424 | 1424 | { |
|
| 1425 | 1425 | options.fallback_tokens.push(t); |
|
| 1426 | + | options.fallback_on_401_only = true; |
|
| 1426 | 1427 | } |
|
| 1427 | 1428 | } |
|
| 1428 | 1429 | options.token = options.token.take().or(stored.token); |
|
| … | 1494 unchanged lines |
modified crates/oxy-server/tests/http.rs +5 −0
| … | 728 unchanged lines |
||
| 729 | 729 | vec!["good".to_string()], |
|
| 730 | 730 | "the parent's, to fall back on" |
|
| 731 | 731 | ); |
|
| 732 | + | assert!(!options.fallback_on_401_only); |
|
| 732 | 733 | write(&nested, "c.txt", b"child\n"); |
|
| 733 | 734 | child.commit("child work", &[]).unwrap(); |
|
| 734 | 735 | let pushed = child.push("origin", false).unwrap(); |
|
| … | 18 unchanged lines |
||
| 753 | 754 | vec!["stale".to_string()], |
|
| 754 | 755 | "the sign-in only" |
|
| 755 | 756 | ); |
|
| 757 | + | assert!( |
|
| 758 | + | options.fallback_on_401_only, |
|
| 759 | + | "and only for a token the hub does not know: one refused for permission is not swapped" |
|
| 760 | + | ); |
|
| 756 | 761 | write(&nested, "d.txt", b"more\n"); |
|
| 757 | 762 | child.commit("more", &[]).unwrap(); |
|
| 758 | 763 | let err = child.push("origin", false).err().unwrap().to_string(); |
|
| … | 467 unchanged lines |