A shallow clone fetches its checkpoint in parts (refs #137) 80b069facc69
checkpoint-index answers with the checkpoint and its history only; the client asks for the graph blobs it lacks with objects, 2048 at a time, so neither side holds a big state's whole answer (about 2 GB for VS Code). objects reads in pack order. A server without checkpoint-index answers 404 and the client falls back to checkpoint.
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key e4264085eda8
- When
- · 2026-10-26 10:57 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
80b069facc699214ba6ae4ad9f9c943df79f1089d3502c4b0fe19082098b3252- Depends on
- 2452b4407ab7 252e8cb23fbc 2863456a0f6d 5b9c6ad45f6e 750251b21a96 75167672ebcd 7bd805ffb418 8fd78eb52ad2 982864068a59 9f9e72b93e4e b3c5a59a875a cb8b8082fa73 e02bf0cd0533 e4595038502a f3811eb20bea ff0af1ee299d
- Depended on by
- fa53a2849de8 78b49d4d01f0 7a3cc5149a14 c0d4075cadc0 b5e98094eb8f 642fa213de80
- In bookmarks
- identical-sides main push-notes subrepo-pin-state
- In tags
- v0.10.0 v0.11.0 v0.8.0 v0.9.0
- Edits
- 27 atoms, 0 tree operations, 7 files
Browse the tree at this change
modified CHANGELOG.md +2 −0
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | + | - **A shallow clone fetches its checkpoint in parts (#137).** The checkpoint of a big state came as one answer, the whole state's graph blobs in one body (about 2 GB for the VS Code history), built and held whole on both sides. A server now answers `checkpoint-index` (the checkpoint and its history, no graphs), and the client fetches the graph blobs it lacks with `objects`, 2,048 at a time; `objects` reads them in pack order. An older server answers 404 and gets the one-piece request as before. |
|
| 6 | + | ||
| 5 | 7 | - **A keyless agent follows its owner's newest identity (#82).** Every delegation rotates its owner's identity, so the `author.key` an agent's home was given goes stale, and its changes named an identity two rotations old (verifying only because a grant from before a rotation still counts). With `author.for = ACCOUNT` in the home, the hub sends the account's newest linked identity with each certificate, with its chain back to the grant's key; oxy keeps it in the home (`keys/author.for`), stores the chain beside the change so the grant checks, and authors as it from then on. A repository's own `author.key` pin still wins. |
|
| 6 | 8 | ||
| 7 | 9 | - **Changelogs stop conflicting (#145).** A file marked `merge=union` in `.oxyattributes` (`CHANGELOG.md merge=union`) has its adjacent insertions settled by the checkout itself: both sides kept, the earlier change's first, as `oxy resolve --both` does, and the next commit records the resolution. Only adjacent insertions: where two changes changed the same lines, the markers stay. Oxygen's own changelog is marked so. |
|
| … | 281 unchanged lines |
modified crates/oxy-core/src/protocol.rs +22 −0
| … | 208 unchanged lines |
||
| 209 | 209 | /// A checkpoint of exactly `heads` (spec §16.1 `checkpoint`): the |
|
| 210 | 210 | /// checkpoint object, its payload blob, and the blob's chunks. |
|
| 211 | 211 | fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle>; |
|
| 212 | + | /// A checkpoint of exactly `heads` without its graph blobs (spec §16.1 |
|
| 213 | + | /// `checkpoint-index`, oxygen#137), which the client then asks for in |
|
| 214 | + | /// parts with `objects`. A remote that cannot answer it sends the |
|
| 215 | + | /// whole checkpoint, which does as well. |
|
| 216 | + | fn checkpoint_index(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 217 | + | self.checkpoint(heads) |
|
| 218 | + | } |
|
| 212 | 219 | /// The changes within `depth` dependency steps of `heads` (spec §17). |
|
| 213 | 220 | fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>>; |
|
| 214 | 221 | /// Push changes, bookmark updates, new tags, and optionally a newer |
|
| … | 45 unchanged lines |
||
| 260 | 267 | } |
|
| 261 | 268 | fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 262 | 269 | self.repo.checkpoint_bundle_for(heads) |
|
| 270 | + | } |
|
| 271 | + | fn checkpoint_index(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 272 | + | self.repo.checkpoint_index_for(heads) |
|
| 263 | 273 | } |
|
| 264 | 274 | fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> { |
|
| 265 | 275 | self.repo.shallow_changes(heads, depth) |
|
| … | 426 unchanged lines |
||
| 692 | 702 | self.call(true, "checkpoint", Some(&wire::encode_hashes(heads)))? |
|
| 693 | 703 | }; |
|
| 694 | 704 | let _t = crate::timing::phase("remote: decode the checkpoint"); |
|
| 705 | + | wire::decode_bundle(&body) |
|
| 706 | + | } |
|
| 707 | + | fn checkpoint_index(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 708 | + | let body = { |
|
| 709 | + | let _t = crate::timing::phase("remote: checkpoint index"); |
|
| 710 | + | match self.call(true, "checkpoint-index", Some(&wire::encode_hashes(heads))) { |
|
| 711 | + | Ok(b) => b, |
|
| 712 | + | // A server from before checkpoint-index: the whole answer. |
|
| 713 | + | Err(RepoError::Msg(m)) if m.contains("HTTP 404") => return self.checkpoint(heads), |
|
| 714 | + | Err(e) => return Err(e), |
|
| 715 | + | } |
|
| 716 | + | }; |
|
| 695 | 717 | wire::decode_bundle(&body) |
|
| 696 | 718 | } |
|
| 697 | 719 | fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> { |
|
| … | 1077 unchanged lines |
modified crates/oxy-core/src/repo/checkpoint.rs +35 −13
| … | 112 unchanged lines |
||
| 113 | 113 | write_atomic(&self.checkpoint_pointer(bookmark), r.to_hex().as_bytes()) |
|
| 114 | 114 | } |
|
| 115 | 115 | ||
| 116 | - | /// A bundle holding a checkpoint of exactly `heads` with its payload |
|
| 117 | - | /// (spec §16.1 `checkpoint`); the current bookmark checkpoint is reused |
|
| 118 | - | /// when it matches, otherwise one is made and, when `heads` are the |
|
| 119 | - | /// bookmark's, kept. |
|
| 120 | - | pub fn checkpoint_bundle_for(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 121 | - | let t_find = crate::timing::phase("checkpoint: find or make"); |
|
| 116 | + | /// The checkpoint of exactly `heads`: the bookmark's latest when it |
|
| 117 | + | /// is that state, else one made now (kept as the bookmark's when the |
|
| 118 | + | /// bookmark is at `heads`). |
|
| 119 | + | fn checkpoint_ref_for(&self, heads: &[ChangeHash]) -> Result<ObjectRef> { |
|
| 120 | + | let _t = crate::timing::phase("checkpoint: find or make"); |
|
| 122 | 121 | let wanted = StateObject::new(heads.to_vec()).heads; |
|
| 123 | 122 | let bookmark = self.config.bookmark.clone(); |
|
| 124 | 123 | let current = self.bookmark_heads()?; |
|
| 125 | - | let r = match self.latest_checkpoint(&bookmark)? { |
|
| 124 | + | Ok(match self.latest_checkpoint(&bookmark)? { |
|
| 126 | 125 | Some((r, cp)) if cp.heads == wanted => r, |
|
| 127 | 126 | _ => { |
|
| 128 | 127 | let state = self.load_state(&wanted)?; |
|
| … | 9 unchanged lines |
||
| 138 | 137 | self.store.put(ObjectType::Checkpoint, &cp.encode())? |
|
| 139 | 138 | } |
|
| 140 | 139 | } |
|
| 141 | - | }; |
|
| 142 | - | drop(t_find); |
|
| 143 | - | let _t = crate::timing::phase("checkpoint: gather its objects"); |
|
| 140 | + | }) |
|
| 141 | + | } |
|
| 142 | + | ||
| 143 | + | /// A checkpoint of exactly `heads` without its graph blobs (spec |
|
| 144 | + | /// §16.1 `checkpoint-index`, oxygen#137): the checkpoint object, its |
|
| 145 | + | /// payload blob and the blob's chunks. The client asks for the graph |
|
| 146 | + | /// blobs the payload names in parts, with `objects`, so neither side |
|
| 147 | + | /// holds a big state's whole answer at once. |
|
| 148 | + | pub fn checkpoint_index_for(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 149 | + | let r = self.checkpoint_ref_for(heads)?; |
|
| 144 | 150 | let cp = Checkpoint::decode(&self.store.get_typed(&r, ObjectType::Checkpoint)?)?; |
|
| 145 | 151 | let mut bundle = Bundle::default(); |
|
| 146 | - | for c in Blob::decode(&self.store.get_typed(&cp.payload, ObjectType::Blob)?)?.chunks { |
|
| 147 | - | bundle.objects.push(self.store.get(&c)?); |
|
| 152 | + | let chunks = Blob::decode(&self.store.get_typed(&cp.payload, ObjectType::Blob)?)?.chunks; |
|
| 153 | + | for got in self.store.get_many(&chunks) { |
|
| 154 | + | bundle.objects.push(got?); |
|
| 148 | 155 | } |
|
| 149 | 156 | bundle.objects.push(self.store.get(&cp.payload)?); |
|
| 157 | + | bundle.objects.push((ObjectType::Checkpoint, cp.encode())); |
|
| 158 | + | Ok(bundle) |
|
| 159 | + | } |
|
| 160 | + | ||
| 161 | + | /// A checkpoint of exactly `heads` with its graph blobs and their |
|
| 162 | + | /// chunks, in one answer (spec §16.1 `checkpoint`): what a client |
|
| 163 | + | /// older than `checkpoint-index` asks for. |
|
| 164 | + | pub fn checkpoint_bundle_for(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 165 | + | let mut bundle = self.checkpoint_index_for(heads)?; |
|
| 166 | + | let _t = crate::timing::phase("checkpoint: gather its objects"); |
|
| 167 | + | let cp = match bundle.objects.last() { |
|
| 168 | + | Some((ObjectType::Checkpoint, body)) => Checkpoint::decode(body)?, |
|
| 169 | + | _ => return fail("a checkpoint index without its checkpoint"), |
|
| 170 | + | }; |
|
| 150 | 171 | // The graph blobs and their chunks in two parallel reads (#137): a |
|
| 151 | 172 | // big state's are tens of thousands, and one by one they took six |
|
| 152 | 173 | // minutes for VS Code. |
|
| … | 9 unchanged lines |
||
| 162 | 183 | } |
|
| 163 | 184 | chunks.extend(Blob::decode(body)?.chunks); |
|
| 164 | 185 | } |
|
| 186 | + | let last = bundle.objects.pop(); |
|
| 165 | 187 | for got in self.store.get_many(&chunks) { |
|
| 166 | 188 | bundle.objects.push(got?); |
|
| 167 | 189 | } |
|
| 168 | 190 | bundle.objects.extend(blobs); |
|
| 169 | - | bundle.objects.push((ObjectType::Checkpoint, cp.encode())); |
|
| 191 | + | bundle.objects.extend(last); |
|
| 170 | 192 | Ok(bundle) |
|
| 171 | 193 | } |
|
| 172 | 194 | ||
| … | 80 unchanged lines |
modified crates/oxy-core/src/repo/strata.rs +4 −0
| … | 419 unchanged lines |
||
| 420 | 420 | self.with_mirror(|m| m.checkpoint_bundle_for(heads)) |
|
| 421 | 421 | } |
|
| 422 | 422 | ||
| 423 | + | fn checkpoint_index(&self, heads: &[ChangeHash]) -> Result<Bundle> { |
|
| 424 | + | self.with_mirror(|m| m.checkpoint_index_for(heads)) |
|
| 425 | + | } |
|
| 426 | + | ||
| 423 | 427 | fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> { |
|
| 424 | 428 | self.with_mirror(|m| m.shallow_changes(heads, depth)) |
|
| 425 | 429 | } |
|
| … | 941 unchanged lines |
modified crates/oxy-core/src/repo/sync.rs +63 −18
| … | 59 unchanged lines |
||
| 60 | 60 | /// not a reference. |
|
| 61 | 61 | pub const OBJECTS_RESULT: &str = "objects"; |
|
| 62 | 62 | ||
| 63 | + | /// Graph blobs asked for in one `objects` request when a shallow clone |
|
| 64 | + | /// fetches its checkpoint in parts (#137): their chunk lists and chunks, |
|
| 65 | + | /// a few MiB a request rather than one answer of gigabytes. |
|
| 66 | + | const CHECKPOINT_PART: usize = 2048; |
|
| 67 | + | ||
| 63 | 68 | impl Repo { |
|
| 64 | 69 | // ------------------------------------------------------------ serving (spec §16) |
|
| 65 | 70 | ||
| … | 219 unchanged lines |
||
| 285 | 290 | ||
| 286 | 291 | /// Objects by reference for the `objects` endpoint; a blob brings its chunks. |
|
| 287 | 292 | pub fn objects_for(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> { |
|
| 288 | - | let mut out = Vec::new(); |
|
| 289 | - | for r in refs { |
|
| 290 | - | let (ty, body) = match self.store.get(r) { |
|
| 291 | - | Ok(x) => x, |
|
| 292 | - | Err(StoreError::NotFound(_)) => continue, |
|
| 293 | + | // In two reads in pack order (#137): a checkpoint's graph blobs come |
|
| 294 | + | // this way in parts, tens of thousands for a big state. |
|
| 295 | + | let mut found = Vec::with_capacity(refs.len()); |
|
| 296 | + | for got in self.store.get_many(refs) { |
|
| 297 | + | match got { |
|
| 298 | + | Ok(x) => found.push(x), |
|
| 299 | + | Err(StoreError::NotFound(_)) => {} |
|
| 293 | 300 | Err(e) => return Err(e.into()), |
|
| 294 | - | }; |
|
| 295 | - | if ty == ObjectType::Blob { |
|
| 296 | - | for c in Blob::decode(&body)?.chunks { |
|
| 297 | - | if let Ok((cty, cb)) = self.store.get(&c) { |
|
| 298 | - | out.push((cty, cb)); |
|
| 299 | - | } |
|
| 300 | - | } |
|
| 301 | + | } |
|
| 302 | + | } |
|
| 303 | + | let mut chunks = Vec::new(); |
|
| 304 | + | for (ty, body) in &found { |
|
| 305 | + | if *ty == ObjectType::Blob { |
|
| 306 | + | chunks.extend(Blob::decode(body)?.chunks); |
|
| 307 | + | } |
|
| 308 | + | } |
|
| 309 | + | let mut out = Vec::with_capacity(found.len() + chunks.len()); |
|
| 310 | + | for got in self.store.get_many(&chunks) { |
|
| 311 | + | match got { |
|
| 312 | + | Ok(x) => out.push(x), |
|
| 313 | + | Err(StoreError::NotFound(_)) => {} |
|
| 314 | + | Err(e) => return Err(e.into()), |
|
| 301 | 315 | } |
|
| 302 | - | out.push((ty, body)); |
|
| 303 | 316 | } |
|
| 317 | + | out.extend(found); |
|
| 304 | 318 | Ok(out) |
|
| 305 | 319 | } |
|
| 306 | 320 | ||
| … | 1566 unchanged lines |
||
| 1873 | 1887 | if r.heads.is_empty() { |
|
| 1874 | 1888 | return Ok(0); |
|
| 1875 | 1889 | } |
|
| 1876 | - | // Base: the remote's checkpoint of exactly these heads. |
|
| 1890 | + | // Base: the remote's checkpoint of exactly these heads, without its |
|
| 1891 | + | // graph blobs, which come in parts below (#137). |
|
| 1877 | 1892 | let cp_bundle = { |
|
| 1878 | 1893 | let _t = crate::timing::phase("shallow: checkpoint request"); |
|
| 1879 | - | remote.checkpoint(&r.heads)? |
|
| 1894 | + | remote.checkpoint_index(&r.heads)? |
|
| 1880 | 1895 | }; |
|
| 1881 | 1896 | // In one batch: a big state's checkpoint is tens of thousands of |
|
| 1882 | 1897 | // graph blobs and chunks, which stored one by one were each written |
|
| … | 16 unchanged lines |
||
| 1899 | 1914 | let Some(cp_ref) = cp_ref else { |
|
| 1900 | 1915 | return fail(format!("{remote_name} sent no checkpoint")); |
|
| 1901 | 1916 | }; |
|
| 1917 | + | // The graph blobs the payload names, with their chunks, a part at |
|
| 1918 | + | // a time (#137): one answer for a big state was gigabytes, held |
|
| 1919 | + | // whole on both sides. A remote that sent the whole checkpoint has |
|
| 1920 | + | // left none to ask for. |
|
| 1921 | + | let cp = crate::objects::Checkpoint::decode( |
|
| 1922 | + | &self.store.get_typed(&cp_ref, ObjectType::Checkpoint)?, |
|
| 1923 | + | )?; |
|
| 1924 | + | let graphs: Vec<ObjectRef> = self |
|
| 1925 | + | .checkpoint_graph_blobs(&cp)? |
|
| 1926 | + | .into_iter() |
|
| 1927 | + | .filter(|b| !self.store.has(b)) |
|
| 1928 | + | .collect(); |
|
| 1929 | + | { |
|
| 1930 | + | let _t = crate::timing::phase("shallow: the checkpoint's graphs, in parts"); |
|
| 1931 | + | for part in graphs.chunks(CHECKPOINT_PART) { |
|
| 1932 | + | let objects = remote.objects(part)?; |
|
| 1933 | + | let objects: Vec<(ObjectType, &[u8])> = objects |
|
| 1934 | + | .iter() |
|
| 1935 | + | .filter(|(ty, _)| matches!(ty, ObjectType::Blob | ObjectType::Chunk)) |
|
| 1936 | + | .map(|(ty, body)| (*ty, body.as_slice())) |
|
| 1937 | + | .collect(); |
|
| 1938 | + | let batch = self.store.begin_batch()?; |
|
| 1939 | + | let stored = self.store.put_many(&objects); |
|
| 1940 | + | batch.finish()?; |
|
| 1941 | + | stored?; |
|
| 1942 | + | } |
|
| 1943 | + | if let Some(b) = graphs.iter().find(|b| !self.store.has(b)) { |
|
| 1944 | + | return fail(format!( |
|
| 1945 | + | "{remote_name} did not send graph blob {}", |
|
| 1946 | + | b.short(12) |
|
| 1947 | + | )); |
|
| 1948 | + | } |
|
| 1949 | + | } |
|
| 1902 | 1950 | self.install_checkpoint(&bookmark, &cp_ref)?; |
|
| 1903 | 1951 | let _t_blobs = crate::timing::phase("shallow: the binary files' blobs"); |
|
| 1904 | 1952 | // The blobs of the state's opaque files come with no change when the |
|
| 1905 | 1953 | // window holds none of the changes that set them (#137): fetch them |
|
| 1906 | 1954 | // now, or under `filter.max-blob` leave them to be fetched on read. |
|
| 1907 | - | let cp = crate::objects::Checkpoint::decode( |
|
| 1908 | - | &self.store.get_typed(&cp_ref, ObjectType::Checkpoint)?, |
|
| 1909 | - | )?; |
|
| 1910 | 1955 | let base = State::from_checkpoint_history(&self.read_blob(&cp.payload)?)?; |
|
| 1911 | 1956 | let mut missing: Vec<ObjectRef> = base |
|
| 1912 | 1957 | .opaque_nodes() |
|
| … | 934 unchanged lines |
modified crates/oxy-server/src/lib.rs +9 −0
| … | 150 unchanged lines |
||
| 151 | 151 | Fetch, |
|
| 152 | 152 | Objects, |
|
| 153 | 153 | Checkpoint, |
|
| 154 | + | CheckpointIndex, |
|
| 154 | 155 | Shallow, |
|
| 155 | 156 | Push, |
|
| 156 | 157 | } |
|
| … | 7 unchanged lines |
||
| 164 | 165 | "fetch" => Endpoint::Fetch, |
|
| 165 | 166 | "objects" => Endpoint::Objects, |
|
| 166 | 167 | "checkpoint" => Endpoint::Checkpoint, |
|
| 168 | + | "checkpoint-index" => Endpoint::CheckpointIndex, |
|
| 167 | 169 | "shallow" => Endpoint::Shallow, |
|
| 168 | 170 | "push" => Endpoint::Push, |
|
| 169 | 171 | _ => return None, |
|
| … | 8 unchanged lines |
||
| 178 | 180 | Endpoint::Fetch => "fetch", |
|
| 179 | 181 | Endpoint::Objects => "objects", |
|
| 180 | 182 | Endpoint::Checkpoint => "checkpoint", |
|
| 183 | + | Endpoint::CheckpointIndex => "checkpoint-index", |
|
| 181 | 184 | Endpoint::Shallow => "shallow", |
|
| 182 | 185 | Endpoint::Push => "push", |
|
| 183 | 186 | } |
|
| … | 139 unchanged lines |
||
| 323 | 326 | let _t = oxy_core::timing::phase("serve: encode the checkpoint"); |
|
| 324 | 327 | ok(wire::encode_bundle(&bundle)) |
|
| 325 | 328 | } |
|
| 329 | + | Endpoint::CheckpointIndex => { |
|
| 330 | + | let heads = wire::decode_hashes(&body)?; |
|
| 331 | + | let lock = self.lock_for(name); |
|
| 332 | + | let _guard = lock.lock().unwrap(); |
|
| 333 | + | ok(wire::encode_bundle(&r.checkpoint_index_for(&heads)?)) |
|
| 334 | + | } |
|
| 326 | 335 | Endpoint::Shallow => { |
|
| 327 | 336 | let (heads, depth) = wire::decode_shallow(&body)?; |
|
| 328 | 337 | ok(wire::encode_hashes(&r.shallow_changes(&heads, depth)?)) |
|
| … | 184 unchanged lines |
modified docs/spec.md +2 −1
| … | 904 unchanged lines |
||
| 905 | 905 | | `negotiate` request | `{0: [have], 1: [want]}`; response `{0: [hashes]}` in dependency order | |
|
| 906 | 906 | | `fetch` request | `{0: [hashes], 1: max blob size or null}`; response a `Bundle` without blobs above the size; also when it names no change, the bundle carries the identities of the keys that signed the attestations served with `refs` (each bookmark's latest and each tag's) and, for a short-lived key, its certificate and its issuer's identity (§14.7), each once, so a client can check an attestation whose signer signed none of the changes it receives, such as a delegate's or a keyless agent's bookmark move (oxygenhub#266) | |
|
| 907 | 907 | | `objects` request | `{0: [refs]}`; response a `Bundle` carrying only objects, a blob accompanied by its chunks | |
|
| 908 | - | | `checkpoint` request | `{0: [heads]}`; response a `Bundle` of objects: the checkpoint of exactly those heads, its payload blob, and the blob's chunks | |
|
| 908 | + | | `checkpoint` request | `{0: [heads]}`; response a `Bundle` of objects: the checkpoint of exactly those heads, its payload blob, and the blob's chunks, with the payload's graph blobs and their chunks (§11) | |
|
| 909 | + | | `checkpoint-index` request | as `checkpoint`, without the graph blobs: the client asks for those it lacks with `objects`, in parts (oxygen#137), so neither side holds a big state's whole answer; a server without it answers 404 and the client falls back to `checkpoint` | |
|
| 909 | 910 | | `shallow` request | `{0: [heads], 1: depth}`; response `{0: [hashes]}`, the changes within `depth` steps of the heads, dependencies first | |
|
| 910 | 911 | | `push` request | `{0: Bundle, 1: [{0: name, 1: old state, 2: new state, 3: force, 4: signed attestation or null}], 2: signed policy or null, 3: [tag]}`; response `{0: [{0: name, 1: ok, 2: reason}]}`, where a result named `policy` reports the policy update and `tags/<name>` each tag | |
|
| 911 | 912 | ||
| … | 280 unchanged lines |