mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Unify blob hash vocabulary
This commit is contained in:
parent
4e48d2887e
commit
bf4265e1b8
30 changed files with 194 additions and 188 deletions
|
|
@ -77,7 +77,7 @@ Emitted when the run record is created.
|
|||
| `source_directory` | string? | Submitter-side source directory |
|
||||
| `workflow_slug` | string? | Workflow slug |
|
||||
| `provenance` | object | Actor and request provenance |
|
||||
| `manifest_blob` | string? | Blob id for the submitted manifest |
|
||||
| `manifest_blob` | string? | Blob hash for the submitted manifest |
|
||||
| `git` | object? | Git provenance observed before the run: normalized `origin_url`, `branch`, optional `sha`, and `dirty` status |
|
||||
| `fork_source_ref` | object? | Source run/checkpoint reference when this run was forked |
|
||||
| `in_place` | boolean | Whether the run was created with `--in-place` (no git checkpoints) |
|
||||
|
|
|
|||
|
|
@ -219,10 +219,10 @@ When Fabro builds a [preamble](/execution/context#preamble-construction) for a d
|
|||
- **plan**: success
|
||||
- Model: claude-sonnet-4-5, 12.4k tokens in / 3.2k out
|
||||
- Files: src/main.rs, tests/api_test.rs
|
||||
- Response: See: /path/to/runtime/blobs/<blob_id>.json
|
||||
- Response: See: /path/to/runtime/blobs/<blob_hash>.json
|
||||
- **test**: success
|
||||
- Script: `cargo test 2>&1 || true`
|
||||
- Stdout: See: /path/to/runtime/blobs/<blob_id>.json
|
||||
- Stdout: See: /path/to/runtime/blobs/<blob_hash>.json
|
||||
```
|
||||
|
||||
This keeps preambles concise while still giving agents a path to read the full output if needed.
|
||||
|
|
@ -237,7 +237,7 @@ Captured stage artifacts such as screenshots, videos, reports, and traces still
|
|||
|
||||
For remote sandboxes (Docker, Daytona), execution-time file access happens inside the sandbox filesystem.
|
||||
|
||||
- Blob refs are materialized into `{working_directory}/.fabro/blobs/{blob_id}.json`
|
||||
- Blob refs are materialized into `{working_directory}/.fabro/blobs/{blob_hash}.json`
|
||||
- Explicit non-blob `file://` refs keep the existing copy-on-demand behavior and are copied into `{working_directory}/.fabro/artifacts/{filename}` when needed
|
||||
|
||||
In both cases, downstream handlers and agents continue to consume ordinary `file://` pointers during execution.
|
||||
|
|
|
|||
|
|
@ -3092,7 +3092,7 @@ paths:
|
|||
operationId: writeRunBlob
|
||||
tags: [Run Internals]
|
||||
summary: Write Run Blob
|
||||
description: Writes an opaque binary blob and returns its content-addressed blob identifier.
|
||||
description: Writes an opaque binary blob and returns its content-addressed blob hash.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RunId"
|
||||
requestBody:
|
||||
|
|
@ -3137,15 +3137,15 @@ paths:
|
|||
schema:
|
||||
$ref: "#/components/schemas/ErrorResponse"
|
||||
|
||||
/api/v1/runs/{id}/blobs/{blobId}:
|
||||
/api/v1/runs/{id}/blobs/{blobHash}:
|
||||
get:
|
||||
operationId: readRunBlob
|
||||
tags: [Run Internals]
|
||||
summary: Read Run Blob
|
||||
description: Reads a previously stored blob by identifier.
|
||||
description: Reads a previously stored blob by hash.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/RunId"
|
||||
- $ref: "#/components/parameters/BlobId"
|
||||
- $ref: "#/components/parameters/BlobHash"
|
||||
responses:
|
||||
"200":
|
||||
description: Blob contents
|
||||
|
|
@ -5974,11 +5974,11 @@ components:
|
|||
default: 65536
|
||||
example: 65536
|
||||
|
||||
BlobId:
|
||||
name: blobId
|
||||
BlobHash:
|
||||
name: blobHash
|
||||
in: path
|
||||
required: true
|
||||
description: Content-addressed blob identifier.
|
||||
description: Content-addressed blob hash.
|
||||
schema:
|
||||
type: string
|
||||
pattern: '^[0-9a-f]{64}$'
|
||||
|
|
@ -10284,15 +10284,15 @@ components:
|
|||
example: 42
|
||||
|
||||
WriteBlobResponse:
|
||||
description: Content-addressed identifier for a stored blob.
|
||||
description: Content-addressed hash of a stored blob.
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- hash
|
||||
properties:
|
||||
id:
|
||||
hash:
|
||||
type: string
|
||||
description: Blob identifier.
|
||||
example: 550e8400-e29b-41d4-a716-446655440000
|
||||
description: Content-addressed hash of the stored blob.
|
||||
example: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
|
||||
|
||||
CommandTermination:
|
||||
description: Terminal state for a command execution.
|
||||
|
|
|
|||
|
|
@ -243,8 +243,8 @@ Checkpoints and checkpoint-completed events persist these `blob://` refs, not ho
|
|||
|
||||
Before Fabro builds a preamble or starts the next stage, it resolves any blob refs into execution-local files so handlers and agents still see normal `file://` references:
|
||||
|
||||
- Local execution materializes blobs under `{run_dir}/runtime/blobs/{blob_id}.json`
|
||||
- Remote sandboxes materialize blobs under `{working_directory}/.fabro/blobs/{blob_id}.json`
|
||||
- Local execution materializes blobs under `{run_dir}/runtime/blobs/{blob_hash}.json`
|
||||
- Remote sandboxes materialize blobs under `{working_directory}/.fabro/blobs/{blob_hash}.json`
|
||||
|
||||
These materialized `file://` paths are runtime-only. They are not written back into durable context snapshots.
|
||||
|
||||
|
|
|
|||
|
|
@ -86,8 +86,8 @@ async fn write_run_dump(
|
|||
dump.add_file_bytes("run.log", log);
|
||||
}
|
||||
|
||||
dump.hydrate_referenced_blobs_with_reader(|blob_id| {
|
||||
Box::pin(async move { client.read_run_blob(run_id, &blob_id).await })
|
||||
dump.hydrate_referenced_blobs_with_reader(|blob_hash| {
|
||||
Box::pin(async move { client.read_run_blob(run_id, &blob_hash).await })
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -325,11 +325,11 @@ async fn resolve_response_string(
|
|||
run_id: &RunId,
|
||||
response: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let Some(blob_id) = blob_id_from_response(response) else {
|
||||
let Some(blob_hash) = blob_hash_from_response(response) else {
|
||||
return Ok(Some(response.to_string()));
|
||||
};
|
||||
|
||||
let Some(bytes) = client.read_run_blob(run_id, &blob_id).await? else {
|
||||
let Some(bytes) = client.read_run_blob(run_id, &blob_hash).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let value: serde_json::Value =
|
||||
|
|
@ -341,7 +341,7 @@ async fn resolve_response_string(
|
|||
}))
|
||||
}
|
||||
|
||||
fn blob_id_from_response(response: &str) -> Option<BlobHash> {
|
||||
fn blob_hash_from_response(response: &str) -> Option<BlobHash> {
|
||||
parse_blob_ref(response)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1022,8 +1022,8 @@ impl RunStoreBackend for HttpRunStore {
|
|||
self.with_retries("read run blob", || {
|
||||
let client = self.client.clone_for_reuse();
|
||||
let run_id = self.run_id;
|
||||
let blob_id = *id;
|
||||
async move { client.read_run_blob(&run_id, &blob_id).await }
|
||||
let blob_hash = *id;
|
||||
async move { client.read_run_blob(&run_id, &blob_hash).await }
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,13 +70,13 @@ fn normalize_attach_json_progress_event(mut event: Value) -> Value {
|
|||
if properties.contains_key("manifest_blob") {
|
||||
properties.insert(
|
||||
"manifest_blob".to_string(),
|
||||
Value::String("[BLOB_ID]".to_string()),
|
||||
Value::String("[BLOB_HASH]".to_string()),
|
||||
);
|
||||
}
|
||||
if properties.contains_key("definition_blob") {
|
||||
properties.insert(
|
||||
"definition_blob".to_string(),
|
||||
Value::String("[BLOB_ID]".to_string()),
|
||||
Value::String("[BLOB_HASH]".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -896,7 +896,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
}
|
||||
}
|
||||
},
|
||||
"manifest_blob": "[BLOB_ID]",
|
||||
"manifest_blob": "[BLOB_HASH]",
|
||||
"provenance": {
|
||||
"client": {
|
||||
"name": "fabro-cli",
|
||||
|
|
@ -1036,7 +1036,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"event": "run.submitted",
|
||||
"id": "[EVENT_ID]",
|
||||
"properties": {
|
||||
"definition_blob": "[BLOB_ID]"
|
||||
"definition_blob": "[BLOB_HASH]"
|
||||
},
|
||||
"run_id": "[ULID]",
|
||||
"ts": "[TIMESTAMP]"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use strum::IntoStaticStr;
|
|||
use crate::auth::{AuthErrorCode, JwtError, REFRESH_TOKEN_PREFIX};
|
||||
use crate::error::ApiError;
|
||||
use crate::jwt_auth::{self, AuthMode, ConfiguredAuth};
|
||||
use crate::server::{AppState, parse_blob_id_path, parse_run_id_path, parse_stage_id_path};
|
||||
use crate::server::{AppState, parse_blob_hash_path, parse_run_id_path, parse_stage_id_path};
|
||||
use crate::worker_token::{self, WORKER_TOKEN_KID, WorkerScopeSet};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -295,14 +295,14 @@ impl FromRequestParts<Arc<AppState>> for RequireRunBlob {
|
|||
parts: &mut Parts,
|
||||
state: &Arc<AppState>,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let Path((id, blob_id)): Path<(String, String)> = Path::from_request_parts(parts, state)
|
||||
let Path((id, blob_hash)): Path<(String, String)> = Path::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(IntoResponse::into_response)?;
|
||||
let run_id = parse_run_id_path(&id)?;
|
||||
let blob_id = parse_blob_id_path(&blob_id)?;
|
||||
let blob_hash = parse_blob_hash_path(&blob_hash)?;
|
||||
require_worker_or_user_for_run(&auth_slot_from_parts(parts), &run_id)
|
||||
.map_err(IntoResponse::into_response)?;
|
||||
Ok(Self(run_id, blob_id))
|
||||
Ok(Self(run_id, blob_hash))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2889,11 +2889,11 @@ pub(crate) fn parse_stage_id_path(stage_id: &str) -> Result<StageId, Response> {
|
|||
|
||||
#[allow(
|
||||
clippy::result_large_err,
|
||||
reason = "Blob ID parsing returns HTTP 400 responses directly."
|
||||
reason = "Blob hash parsing returns HTTP 400 responses directly."
|
||||
)]
|
||||
pub(crate) fn parse_blob_id_path(blob_id: &str) -> Result<BlobHash, Response> {
|
||||
BlobHash::from_str(blob_id)
|
||||
.map_err(|_| ApiError::bad_request("Invalid blob ID.").into_response())
|
||||
pub(crate) fn parse_blob_hash_path(blob_hash: &str) -> Result<BlobHash, Response> {
|
||||
BlobHash::from_str(blob_hash)
|
||||
.map_err(|_| ApiError::bad_request("Invalid blob hash.").into_response())
|
||||
}
|
||||
|
||||
#[allow(
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pub(super) fn routes() -> Router<Arc<AppState>> {
|
|||
Router::new()
|
||||
.route("/runs/{id}/checkpoint", get(get_checkpoint))
|
||||
.route("/runs/{id}/blobs", post(write_run_blob))
|
||||
.route("/runs/{id}/blobs/{blobId}", get(read_run_blob))
|
||||
.route("/runs/{id}/blobs/{blobHash}", get(read_run_blob))
|
||||
.route("/runs/{id}/artifacts", get(list_run_artifacts))
|
||||
.route("/runs/{id}/artifacts/download", get(download_run_artifacts))
|
||||
.route(
|
||||
|
|
@ -105,8 +105,8 @@ async fn write_run_blob(
|
|||
}
|
||||
match state.stores.runs.open_run(&id).await {
|
||||
Ok(run_store) => match run_store.write_blob(&body).await {
|
||||
Ok(blob_id) => Json(WriteBlobResponse {
|
||||
id: blob_id.to_string(),
|
||||
Ok(blob_hash) => Json(WriteBlobResponse {
|
||||
hash: blob_hash.to_string(),
|
||||
})
|
||||
.into_response(),
|
||||
Err(err) => {
|
||||
|
|
@ -118,11 +118,11 @@ async fn write_run_blob(
|
|||
}
|
||||
|
||||
async fn read_run_blob(
|
||||
RequireRunBlob(id, blob_id): RequireRunBlob,
|
||||
RequireRunBlob(id, blob_hash): RequireRunBlob,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Response {
|
||||
match state.stores.runs.open_run_reader(&id).await {
|
||||
Ok(run_store) => match run_store.read_blob(&blob_id).await {
|
||||
Ok(run_store) => match run_store.read_blob(&blob_hash).await {
|
||||
Ok(Some(bytes)) => octet_stream_response(bytes),
|
||||
Ok(None) => ApiError::not_found("Blob not found.").into_response(),
|
||||
Err(err) => {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ pub(super) fn demo_routes() -> Router<Arc<AppState>> {
|
|||
)
|
||||
.route("/runs/{id}/attach", get(demo::run_events_stub))
|
||||
.route("/runs/{id}/blobs", post(not_implemented))
|
||||
.route("/runs/{id}/blobs/{blobId}", get(not_implemented))
|
||||
.route("/runs/{id}/blobs/{blobHash}", get(not_implemented))
|
||||
.route(
|
||||
"/runs/{id}/stages/{stageId}/logs/output",
|
||||
get(not_implemented),
|
||||
|
|
|
|||
|
|
@ -11057,11 +11057,11 @@ async fn write_and_read_run_blob_round_trip() {
|
|||
.unwrap();
|
||||
let response = app.clone().oneshot(req).await.unwrap();
|
||||
let body = response_json!(response, StatusCode::OK).await;
|
||||
let blob_id = body["id"].as_str().unwrap();
|
||||
let blob_hash = body["hash"].as_str().unwrap();
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api(&format!("/runs/{run_id}/blobs/{blob_id}")))
|
||||
.uri(api(&format!("/runs/{run_id}/blobs/{blob_hash}")))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let response = app.oneshot(req).await.unwrap();
|
||||
|
|
@ -11459,7 +11459,7 @@ async fn worker_token_accepts_run_scoped_routes_and_falls_back_to_user_jwt() {
|
|||
let worker_token = issue_test_worker_token(&run_id);
|
||||
let other_run_id = create_run_with_bearer(&app, &user_jwt).await;
|
||||
let other_worker_token = issue_test_worker_token(&other_run_id);
|
||||
let blob_id = state
|
||||
let blob_hash = state
|
||||
.stores
|
||||
.runs
|
||||
.open_run(&run_id)
|
||||
|
|
@ -11553,7 +11553,7 @@ async fn worker_token_accepts_run_scoped_routes_and_falls_back_to_user_jwt() {
|
|||
.clone()
|
||||
.oneshot(bearer_request(
|
||||
Method::GET,
|
||||
&format!("/runs/{run_id}/blobs/{blob_id}"),
|
||||
&format!("/runs/{run_id}/blobs/{blob_hash}"),
|
||||
&worker_token,
|
||||
Body::empty(),
|
||||
))
|
||||
|
|
@ -12058,7 +12058,7 @@ async fn worker_token_is_rejected_on_user_only_routes() {
|
|||
let user_jwt = issue_test_user_jwt();
|
||||
let run_id = create_run_with_bearer(&app, &user_jwt).await;
|
||||
let worker_token = issue_test_worker_token(&run_id);
|
||||
let blob_id = BlobHash::new(b"blob");
|
||||
let blob_hash = BlobHash::new(b"blob");
|
||||
let user_only_routes = vec![
|
||||
(Method::GET, "/runs".to_string()),
|
||||
(Method::POST, "/runs".to_string()),
|
||||
|
|
@ -12121,7 +12121,7 @@ async fn worker_token_is_rejected_on_user_only_routes() {
|
|||
.clone()
|
||||
.oneshot(bearer_request(
|
||||
Method::GET,
|
||||
&format!("/runs/{run_id}/blobs/{blob_id}"),
|
||||
&format!("/runs/{run_id}/blobs/{blob_hash}"),
|
||||
&worker_token,
|
||||
Body::empty(),
|
||||
))
|
||||
|
|
|
|||
|
|
@ -214,30 +214,30 @@ impl RunDump {
|
|||
for entry in &mut self.entries {
|
||||
match &mut entry.contents {
|
||||
RunDumpContents::Json(value) => {
|
||||
let mut blob_ids = Vec::new();
|
||||
collect_blob_refs_in_value(value, &mut blob_ids);
|
||||
for blob_id in blob_ids {
|
||||
if cache.contains_key(&blob_id) {
|
||||
let mut blob_hashes = Vec::new();
|
||||
collect_blob_refs_in_value(value, &mut blob_hashes);
|
||||
for blob_hash in blob_hashes {
|
||||
if cache.contains_key(&blob_hash) {
|
||||
continue;
|
||||
}
|
||||
let blob = read_blob(blob_id).await?.with_context(|| {
|
||||
format!("blob {blob_id:?} is missing from the store")
|
||||
let blob = read_blob(blob_hash).await?.with_context(|| {
|
||||
format!("blob {blob_hash:?} is missing from the store")
|
||||
})?;
|
||||
let hydrated: serde_json::Value = serde_json::from_slice(&blob)
|
||||
.with_context(|| format!("blob {blob_id:?} is not valid JSON"))?;
|
||||
cache.insert(blob_id, hydrated);
|
||||
.with_context(|| format!("blob {blob_hash:?} is not valid JSON"))?;
|
||||
cache.insert(blob_hash, hydrated);
|
||||
}
|
||||
replace_blob_refs_in_value(value, &cache)?;
|
||||
}
|
||||
RunDumpContents::Text(text) => {
|
||||
let Some(blob_id) = parse_blob_ref(text) else {
|
||||
let Some(blob_hash) = parse_blob_ref(text) else {
|
||||
continue;
|
||||
};
|
||||
let blob = read_blob(blob_id)
|
||||
let blob = read_blob(blob_hash)
|
||||
.await?
|
||||
.with_context(|| format!("blob {blob_id:?} is missing from the store"))?;
|
||||
.with_context(|| format!("blob {blob_hash:?} is missing from the store"))?;
|
||||
*text = serde_json::from_slice::<String>(&blob).with_context(|| {
|
||||
format!("blob {blob_id:?} is not a JSON string text log")
|
||||
format!("blob {blob_hash:?} is not a JSON string text log")
|
||||
})?;
|
||||
}
|
||||
RunDumpContents::Bytes(_) => {}
|
||||
|
|
@ -386,21 +386,21 @@ fn validate_relative_path(kind: &str, value: &str) -> Result<PathBuf> {
|
|||
Ok(normalized)
|
||||
}
|
||||
|
||||
fn collect_blob_refs_in_value(value: &serde_json::Value, blob_ids: &mut Vec<BlobHash>) {
|
||||
fn collect_blob_refs_in_value(value: &serde_json::Value, blob_hashes: &mut Vec<BlobHash>) {
|
||||
match value {
|
||||
serde_json::Value::String(current) => {
|
||||
if let Some(blob_id) = parse_blob_ref(current) {
|
||||
blob_ids.push(blob_id);
|
||||
if let Some(blob_hash) = parse_blob_ref(current) {
|
||||
blob_hashes.push(blob_hash);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(items) => {
|
||||
for item in items {
|
||||
collect_blob_refs_in_value(item, blob_ids);
|
||||
collect_blob_refs_in_value(item, blob_hashes);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Object(map) => {
|
||||
for item in map.values() {
|
||||
collect_blob_refs_in_value(item, blob_ids);
|
||||
collect_blob_refs_in_value(item, blob_hashes);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {}
|
||||
|
|
@ -413,13 +413,12 @@ fn replace_blob_refs_in_value(
|
|||
) -> Result<()> {
|
||||
match value {
|
||||
serde_json::Value::String(current) => {
|
||||
let Some(blob_id) = parse_blob_ref(current) else {
|
||||
let Some(blob_hash) = parse_blob_ref(current) else {
|
||||
return Ok(());
|
||||
};
|
||||
let hydrated = cache
|
||||
.get(&blob_id)
|
||||
.cloned()
|
||||
.with_context(|| format!("blob {blob_id:?} is missing from the hydration cache"))?;
|
||||
let hydrated = cache.get(&blob_hash).cloned().with_context(|| {
|
||||
format!("blob {blob_hash:?} is missing from the hydration cache")
|
||||
})?;
|
||||
*value = hydrated;
|
||||
}
|
||||
serde_json::Value::Array(items) => {
|
||||
|
|
@ -724,8 +723,8 @@ mod tests {
|
|||
#[test]
|
||||
fn hydrate_referenced_blobs_ignores_legacy_artifact_file_refs() {
|
||||
let blob = serde_json::to_vec("hydrated legacy text").unwrap();
|
||||
let blob_id = fabro_types::BlobHash::new(&blob);
|
||||
let legacy_ref = format!("file:///sandbox/.fabro/artifacts/{blob_id}.json");
|
||||
let blob_hash = fabro_types::BlobHash::new(&blob);
|
||||
let legacy_ref = format!("file:///sandbox/.fabro/artifacts/{blob_hash}.json");
|
||||
let mut dump = RunDump {
|
||||
entries: vec![RunDumpEntry::json(
|
||||
"run.json",
|
||||
|
|
@ -736,10 +735,10 @@ mod tests {
|
|||
};
|
||||
|
||||
executor::block_on(async {
|
||||
dump.hydrate_referenced_blobs_with_reader(|read_blob_id| {
|
||||
dump.hydrate_referenced_blobs_with_reader(|read_blob_hash| {
|
||||
let blob = blob.clone();
|
||||
Box::pin(async move {
|
||||
assert_eq!(read_blob_id, blob_id);
|
||||
assert_eq!(read_blob_hash, blob_hash);
|
||||
Ok(Some(bytes::Bytes::from(blob)))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -836,12 +836,12 @@ mod tests {
|
|||
append_created(&run_2, "run-2", dt("2026-03-27T12:00:10Z")).await;
|
||||
|
||||
let shared_blob = br#"{"summary":"shared"}"#;
|
||||
let shared_blob_id = run_1.write_blob(shared_blob).await.unwrap();
|
||||
let shared_blob_hash = run_1.write_blob(shared_blob).await.unwrap();
|
||||
|
||||
store.delete_run(&test_run_id("run-1")).await.unwrap();
|
||||
|
||||
let reopened = store.open_run(&test_run_id("run-2")).await.unwrap();
|
||||
let read = reopened.read_blob(&shared_blob_id).await.unwrap();
|
||||
let read = reopened.read_blob(&shared_blob_hash).await.unwrap();
|
||||
assert_eq!(read.as_deref(), Some(shared_blob.as_slice()));
|
||||
}
|
||||
|
||||
|
|
@ -851,7 +851,7 @@ mod tests {
|
|||
let run = store.create_run(&test_run_id("run-1")).await.unwrap();
|
||||
append_created(&run, "run-1", dt("2026-03-27T12:00:00Z")).await;
|
||||
let blob = br#"{"summary":"readable"}"#;
|
||||
let blob_id = run.write_blob(blob).await.unwrap();
|
||||
let blob_hash = run.write_blob(blob).await.unwrap();
|
||||
|
||||
// Evict the cached writer so the reader is built through the real
|
||||
// `open_run_reader` construction path, not a clone of the writer.
|
||||
|
|
@ -859,7 +859,7 @@ mod tests {
|
|||
|
||||
let reader = store.open_run_reader(&test_run_id("run-1")).await.unwrap();
|
||||
assert_eq!(
|
||||
reader.read_blob(&blob_id).await.unwrap().as_deref(),
|
||||
reader.read_blob(&blob_hash).await.unwrap().as_deref(),
|
||||
Some(blob.as_slice())
|
||||
);
|
||||
let err = reader.write_blob(b"blocked").await.unwrap_err();
|
||||
|
|
|
|||
|
|
@ -86,10 +86,10 @@ impl WorkflowVersionStore {
|
|||
&self,
|
||||
id: &WorkflowVersionId,
|
||||
) -> Result<Option<ValidatedWorkflowVersion>, WorkflowVersionStoreError> {
|
||||
let blob_id = (*id).into();
|
||||
let blob_hash = (*id).into();
|
||||
let Some(bytes) = self
|
||||
.blobs
|
||||
.read(&blob_id)
|
||||
.read(&blob_hash)
|
||||
.await
|
||||
.map_err(|source| WorkflowVersionStoreError::Storage { source })?
|
||||
else {
|
||||
|
|
@ -201,8 +201,11 @@ mod tests {
|
|||
|
||||
let id = store.put(&version).await.unwrap();
|
||||
assert_eq!(id, expected_id);
|
||||
let blob_id = id.into();
|
||||
assert_eq!(blobs.read(&blob_id).await.unwrap().unwrap(), expected_bytes);
|
||||
let blob_hash = id.into();
|
||||
assert_eq!(
|
||||
blobs.read(&blob_hash).await.unwrap().unwrap(),
|
||||
expected_bytes
|
||||
);
|
||||
assert_eq!(store.get(&id).await.unwrap(), Some(version));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const ARTIFACT_POINTER_PREFIX: &str = "file://";
|
|||
///
|
||||
/// For each entry in `updates` whose serialized JSON exceeds
|
||||
/// `BLOB_OFFLOAD_THRESHOLD`, the value is persisted as a blob in `run_store`
|
||||
/// and replaced with a `"blob://sha256/{blob_id}"` reference.
|
||||
/// and replaced with a `"blob://sha256/{blob_hash}"` reference.
|
||||
/// Small values are left untouched.
|
||||
///
|
||||
/// `parallel.results` is offloaded at each branch context-update boundary
|
||||
|
|
@ -102,11 +102,11 @@ async fn offload_value(value: &mut Value, run_store: &RunStoreHandle) -> Result<
|
|||
.map_err(|e| Error::engine_with_source("artifact serialize failed", e))?;
|
||||
|
||||
if bytes.len() > BLOB_OFFLOAD_THRESHOLD {
|
||||
let blob_id = run_store
|
||||
let blob_hash = run_store
|
||||
.write_blob(&bytes)
|
||||
.await
|
||||
.map_err(|e| Error::engine_with_anyhow("artifact blob write failed", e))?;
|
||||
*value = Value::String(format_blob_ref(&blob_id));
|
||||
*value = Value::String(format_blob_ref(&blob_hash));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -232,17 +232,17 @@ pub async fn resolve_text_or_blob_ref(value: &Value, run_store: &RunStoreHandle)
|
|||
/// blob reference.
|
||||
///
|
||||
/// Managed `file://` references are normalized through their content-addressed
|
||||
/// blob id instead of reading an execution-local path. Ordinary strings and
|
||||
/// blob hash instead of reading an execution-local path. Ordinary strings and
|
||||
/// ordinary file references remain unchanged for the caller to validate.
|
||||
pub(crate) async fn resolve_json_value(value: Value, run_store: &RunStoreHandle) -> Result<Value> {
|
||||
let blob_id = value.as_str().and_then(|reference| {
|
||||
let blob_hash = value.as_str().and_then(|reference| {
|
||||
parse_blob_ref(reference).or_else(|| parse_managed_blob_file_ref(reference))
|
||||
});
|
||||
let Some(blob_id) = blob_id else {
|
||||
let Some(blob_hash) = blob_hash else {
|
||||
return Ok(value);
|
||||
};
|
||||
|
||||
let bytes = read_required_blob(&blob_id, run_store).await?;
|
||||
let bytes = read_required_blob(&blob_hash, run_store).await?;
|
||||
serde_json::from_slice(&bytes)
|
||||
.map_err(|err| Error::engine_with_source("artifact blob was not valid JSON", err))
|
||||
}
|
||||
|
|
@ -267,14 +267,14 @@ pub async fn resolve_text_or_blob_ref_str(
|
|||
current: &str,
|
||||
run_store: &RunStoreHandle,
|
||||
) -> Result<String> {
|
||||
let Some(blob_id) = parse_blob_ref(current) else {
|
||||
let Some(blob_hash) = parse_blob_ref(current) else {
|
||||
return Ok(current.to_string());
|
||||
};
|
||||
let bytes = run_store
|
||||
.read_blob(&blob_id)
|
||||
.read_blob(&blob_hash)
|
||||
.await
|
||||
.map_err(|e| Error::engine_with_anyhow("text blob read failed", e))?
|
||||
.ok_or_else(|| Error::engine(format!("text blob missing: {blob_id}")))?;
|
||||
.ok_or_else(|| Error::engine(format!("text blob missing: {blob_hash}")))?;
|
||||
serde_json::from_slice::<String>(&bytes)
|
||||
.map_err(|e| Error::engine_with_source("text blob was not a JSON string", e))
|
||||
}
|
||||
|
|
@ -334,8 +334,8 @@ pub async fn sync_artifacts_to_env(
|
|||
fn normalize_durable_value(value: &mut Value) {
|
||||
match value {
|
||||
Value::String(current) => {
|
||||
if let Some(blob_id) = parse_managed_blob_file_ref(current) {
|
||||
*current = format_blob_ref(&blob_id);
|
||||
if let Some(blob_hash) = parse_managed_blob_file_ref(current) {
|
||||
*current = format_blob_ref(&blob_hash);
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
|
|
@ -382,8 +382,8 @@ fn resolve_execution_value<'a>(
|
|||
Value::String(current) => {
|
||||
if key.is_some_and(is_text_context_key) {
|
||||
*current = resolve_text_or_blob_ref_str(current, run_store).await?;
|
||||
} else if let Some(blob_id) = parse_blob_ref(current) {
|
||||
*current = materialize_blob_ref(&blob_id, run_store, env, run_dir).await?;
|
||||
} else if let Some(blob_hash) = parse_blob_ref(current) {
|
||||
*current = materialize_blob_ref(&blob_hash, run_store, env, run_dir).await?;
|
||||
} else if current.starts_with(ARTIFACT_POINTER_PREFIX)
|
||||
&& parse_managed_blob_file_ref(current).is_none()
|
||||
{
|
||||
|
|
@ -413,7 +413,7 @@ fn resolve_execution_value<'a>(
|
|||
}
|
||||
|
||||
async fn materialize_blob_ref(
|
||||
blob_id: &BlobHash,
|
||||
blob_hash: &BlobHash,
|
||||
run_store: &RunStoreHandle,
|
||||
env: &dyn Sandbox,
|
||||
run_dir: &Path,
|
||||
|
|
@ -421,9 +421,9 @@ async fn materialize_blob_ref(
|
|||
// Blobs are content-addressed, so an existing materialized file is always
|
||||
// current — check before paying for the store read.
|
||||
if is_local_execution(env, run_dir).await? {
|
||||
let path = local_materialized_blob_path(run_dir, blob_id);
|
||||
let path = local_materialized_blob_path(run_dir, blob_hash);
|
||||
if !path.exists() {
|
||||
let bytes = read_required_blob(blob_id, run_store).await?;
|
||||
let bytes = read_required_blob(blob_hash, run_store).await?;
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).await.map_err(|err| {
|
||||
Error::Io(format!(
|
||||
|
|
@ -439,13 +439,13 @@ async fn materialize_blob_ref(
|
|||
return Ok(format!("{ARTIFACT_POINTER_PREFIX}{}", path.display()));
|
||||
}
|
||||
|
||||
let remote_path = format!("{}/.fabro/blobs/{blob_id}.json", env.working_directory());
|
||||
let remote_path = format!("{}/.fabro/blobs/{blob_hash}.json", env.working_directory());
|
||||
if !env
|
||||
.file_exists(&remote_path)
|
||||
.await
|
||||
.map_err(|e| Error::engine_with_source("failed to check blob existence", e))?
|
||||
{
|
||||
let bytes = read_required_blob(blob_id, run_store).await?;
|
||||
let bytes = read_required_blob(blob_hash, run_store).await?;
|
||||
let content = String::from_utf8(bytes.to_vec())
|
||||
.map_err(|e| Error::engine_with_source("artifact blob was not valid UTF-8 JSON", e))?;
|
||||
env.write_file(&remote_path, &content).await.map_err(|e| {
|
||||
|
|
@ -457,14 +457,14 @@ async fn materialize_blob_ref(
|
|||
}
|
||||
|
||||
async fn read_required_blob(
|
||||
blob_id: &BlobHash,
|
||||
blob_hash: &BlobHash,
|
||||
run_store: &RunStoreHandle,
|
||||
) -> Result<bytes::Bytes> {
|
||||
run_store
|
||||
.read_blob(blob_id)
|
||||
.read_blob(blob_hash)
|
||||
.await
|
||||
.map_err(|e| Error::engine_with_anyhow("artifact blob read failed", e))?
|
||||
.ok_or_else(|| Error::engine(format!("artifact blob missing: {blob_id}")))
|
||||
.ok_or_else(|| Error::engine(format!("artifact blob missing: {blob_hash}")))
|
||||
}
|
||||
|
||||
async fn resolve_explicit_file_ref(value: &str, env: &dyn Sandbox) -> Result<String> {
|
||||
|
|
@ -508,11 +508,11 @@ async fn is_local_execution(env: &dyn Sandbox, run_dir: &Path) -> Result<bool> {
|
|||
.map_err(|e| Error::engine_with_source("failed to inspect sandbox locality", e))
|
||||
}
|
||||
|
||||
fn local_materialized_blob_path(run_dir: &Path, blob_id: &BlobHash) -> PathBuf {
|
||||
fn local_materialized_blob_path(run_dir: &Path, blob_hash: &BlobHash) -> PathBuf {
|
||||
RunScratch::new(run_dir)
|
||||
.runtime_dir()
|
||||
.join("blobs")
|
||||
.join(format!("{blob_id}.json"))
|
||||
.join(format!("{blob_hash}.json"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -549,7 +549,7 @@ mod tests {
|
|||
|
||||
let large_string = "x".repeat(BLOB_OFFLOAD_THRESHOLD + 1);
|
||||
let serialized = serde_json::to_vec(&serde_json::json!(large_string.clone())).unwrap();
|
||||
let expected_blob_id = fabro_types::BlobHash::new(&serialized);
|
||||
let expected_blob_hash = fabro_types::BlobHash::new(&serialized);
|
||||
|
||||
let mut updates = HashMap::new();
|
||||
updates.insert("response.plan".to_string(), serde_json::json!(large_string));
|
||||
|
|
@ -561,11 +561,11 @@ mod tests {
|
|||
let pointer = updates.get("response.plan").unwrap();
|
||||
assert_eq!(
|
||||
pointer,
|
||||
&serde_json::json!(fabro_types::format_blob_ref(&expected_blob_id))
|
||||
&serde_json::json!(fabro_types::format_blob_ref(&expected_blob_hash))
|
||||
);
|
||||
|
||||
let blob = run_store
|
||||
.read_blob(&expected_blob_id)
|
||||
.read_blob(&expected_blob_hash)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("blob should exist");
|
||||
|
|
@ -591,21 +591,21 @@ mod tests {
|
|||
async fn resolve_json_value_hydrates_blob_and_managed_file_references() {
|
||||
let run_store = make_run_store("structured-json-resolution").await;
|
||||
let value = serde_json::json!([{"name": "api"}, {"name": "web"}]);
|
||||
let blob_id = run_store
|
||||
let blob_hash = run_store
|
||||
.write_blob(&serde_json::to_vec(&value).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
let handle = run_store.clone().into();
|
||||
|
||||
assert_eq!(
|
||||
resolve_json_value(serde_json::json!(format_blob_ref(&blob_id)), &handle)
|
||||
resolve_json_value(serde_json::json!(format_blob_ref(&blob_hash)), &handle)
|
||||
.await
|
||||
.unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_json_value(
|
||||
serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_id}.json")),
|
||||
serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_hash}.json")),
|
||||
&handle,
|
||||
)
|
||||
.await
|
||||
|
|
@ -789,13 +789,13 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn normalize_durable_updates_rewrites_managed_blob_file_refs_recursively() {
|
||||
let blob_id = fabro_types::BlobHash::new(b"hello");
|
||||
let blob_hash = fabro_types::BlobHash::new(b"hello");
|
||||
let mut updates = HashMap::from([(
|
||||
"nested".to_string(),
|
||||
serde_json::json!({
|
||||
"items": [
|
||||
format!("file:///tmp/run/runtime/blobs/{blob_id}.json"),
|
||||
format!("file:///sandbox/.fabro/blobs/{blob_id}.json"),
|
||||
format!("file:///tmp/run/runtime/blobs/{blob_hash}.json"),
|
||||
format!("file:///sandbox/.fabro/blobs/{blob_hash}.json"),
|
||||
"file:///tmp/report.json",
|
||||
]
|
||||
}),
|
||||
|
|
@ -807,8 +807,8 @@ mod tests {
|
|||
updates["nested"],
|
||||
serde_json::json!({
|
||||
"items": [
|
||||
fabro_types::format_blob_ref(&blob_id),
|
||||
fabro_types::format_blob_ref(&blob_id),
|
||||
fabro_types::format_blob_ref(&blob_hash),
|
||||
fabro_types::format_blob_ref(&blob_hash),
|
||||
"file:///tmp/report.json",
|
||||
]
|
||||
})
|
||||
|
|
@ -870,7 +870,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn normalize_checkpoint_for_resume_converts_managed_blob_file_refs_and_drops_preamble() {
|
||||
let blob_id = fabro_types::BlobHash::new(b"managed");
|
||||
let blob_hash = fabro_types::BlobHash::new(b"managed");
|
||||
let mut checkpoint = crate::records::Checkpoint {
|
||||
timestamp: chrono::Utc::now(),
|
||||
current_node: "work".to_string(),
|
||||
|
|
@ -883,7 +883,7 @@ mod tests {
|
|||
),
|
||||
(
|
||||
"response.work".to_string(),
|
||||
serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_id}.json")),
|
||||
serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_hash}.json")),
|
||||
),
|
||||
]),
|
||||
node_outcomes: HashMap::from([(
|
||||
|
|
@ -891,7 +891,7 @@ mod tests {
|
|||
crate::outcome::Outcome {
|
||||
context_updates: HashMap::from([(
|
||||
"response.work".to_string(),
|
||||
serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_id}.json")),
|
||||
serde_json::json!(format!("file:///sandbox/.fabro/blobs/{blob_hash}.json")),
|
||||
)]),
|
||||
..crate::outcome::Outcome::success()
|
||||
},
|
||||
|
|
@ -912,14 +912,14 @@ mod tests {
|
|||
);
|
||||
assert_eq!(
|
||||
checkpoint.context_values.get("response.work"),
|
||||
Some(&serde_json::json!(fabro_types::format_blob_ref(&blob_id)))
|
||||
Some(&serde_json::json!(fabro_types::format_blob_ref(&blob_hash)))
|
||||
);
|
||||
assert_eq!(
|
||||
checkpoint
|
||||
.node_outcomes
|
||||
.get("work")
|
||||
.and_then(|outcome| outcome.context_updates.get("response.work")),
|
||||
Some(&serde_json::json!(fabro_types::format_blob_ref(&blob_id)))
|
||||
Some(&serde_json::json!(fabro_types::format_blob_ref(&blob_hash)))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,14 +109,14 @@ pub async fn read_json_string_blob(
|
|||
run_store: &RunStoreHandle,
|
||||
blob_ref: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let Some(blob_id) = fabro_types::parse_blob_ref(blob_ref) else {
|
||||
let Some(blob_hash) = fabro_types::parse_blob_ref(blob_ref) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bytes = run_store
|
||||
.read_blob(&blob_id)
|
||||
.read_blob(&blob_hash)
|
||||
.await
|
||||
.map_err(|err| Error::engine_with_anyhow("command log blob read failed", err))?
|
||||
.ok_or_else(|| Error::engine(format!("command log blob missing: {blob_id}")))?;
|
||||
.ok_or_else(|| Error::engine(format!("command log blob missing: {blob_hash}")))?;
|
||||
let text = serde_json::from_slice::<String>(&bytes)
|
||||
.map_err(|err| Error::engine_with_source("command log blob was not a JSON string", err))?;
|
||||
Ok(Some(text))
|
||||
|
|
@ -155,9 +155,9 @@ async fn write_json_string_blob(run_store: &RunStoreHandle, text: &str) -> Resul
|
|||
let value = Value::String(text.to_string());
|
||||
let bytes = serde_json::to_vec(&value)
|
||||
.map_err(|err| Error::engine_with_source("command log JSON serialization failed", err))?;
|
||||
let blob_id = run_store
|
||||
let blob_hash = run_store
|
||||
.write_blob(&bytes)
|
||||
.await
|
||||
.map_err(|err| Error::engine_with_anyhow("command log blob write failed", err))?;
|
||||
Ok(format_blob_ref(&blob_id))
|
||||
Ok(format_blob_ref(&blob_hash))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -390,12 +390,12 @@ mod tests {
|
|||
}
|
||||
|
||||
async fn write_blob(&self, data: &[u8]) -> anyhow::Result<fabro_types::BlobHash> {
|
||||
let blob_id = fabro_types::BlobHash::new(data);
|
||||
let blob_hash = fabro_types::BlobHash::new(data);
|
||||
self.blobs
|
||||
.lock()
|
||||
.await
|
||||
.insert(blob_id, Bytes::copy_from_slice(data));
|
||||
Ok(blob_id)
|
||||
.insert(blob_hash, Bytes::copy_from_slice(data));
|
||||
Ok(blob_hash)
|
||||
}
|
||||
|
||||
async fn read_blob(&self, id: &fabro_types::BlobHash) -> anyhow::Result<Option<Bytes>> {
|
||||
|
|
|
|||
|
|
@ -1917,7 +1917,7 @@ mod tests {
|
|||
"name": "large-item",
|
||||
"body": "x".repeat(101 * 1024)
|
||||
}]);
|
||||
let blob_id = run_store
|
||||
let blob_hash = run_store
|
||||
.write_blob(&serde_json::to_vec(&items).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -1933,7 +1933,7 @@ mod tests {
|
|||
)));
|
||||
let (node, graph) = for_each_graph("items", 1);
|
||||
let context = test_context();
|
||||
context.set("items", serde_json::json!(format_blob_ref(&blob_id)));
|
||||
context.set("items", serde_json::json!(format_blob_ref(&blob_hash)));
|
||||
|
||||
let outcome = ParallelHandler
|
||||
.execute(&node, &context, &graph, sandbox_dir.path(), &services)
|
||||
|
|
|
|||
|
|
@ -359,8 +359,8 @@ impl RunSession {
|
|||
let git = git_checkpoint_options_from_start(settings, &record.run_id, state.start);
|
||||
let definition_blob = state.spec.definition_blob;
|
||||
let accepted_definition = match definition_blob {
|
||||
Some(blob_id) => {
|
||||
Some(load_accepted_run_definition(&services.run_store, blob_id).await?)
|
||||
Some(blob_hash) => {
|
||||
Some(load_accepted_run_definition(&services.run_store, blob_hash).await?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
|
@ -570,15 +570,15 @@ fn vault_token_lookup(vault: &Vault, name: &str) -> Option<String> {
|
|||
|
||||
async fn load_accepted_run_definition(
|
||||
run_store: &RunStoreHandle,
|
||||
blob_id: fabro_types::BlobHash,
|
||||
blob_hash: fabro_types::BlobHash,
|
||||
) -> Result<RunDefinition, Error> {
|
||||
let bytes = run_store
|
||||
.read_blob(&blob_id)
|
||||
.read_blob(&blob_hash)
|
||||
.await
|
||||
.map_err(|err| Error::engine(err.to_string()))?
|
||||
.ok_or_else(|| {
|
||||
Error::engine(format!(
|
||||
"run definition blob is missing from the run store: {blob_id}"
|
||||
"run definition blob is missing from the run store: {blob_hash}"
|
||||
))
|
||||
})?;
|
||||
serde_json::from_slice(&bytes).map_err(|err| Error::Parse(err.to_string()))
|
||||
|
|
|
|||
|
|
@ -751,11 +751,11 @@ impl HandlerTrait for BlobCommandOutputHandler {
|
|||
services: &crate::handler::EngineServices,
|
||||
) -> std::result::Result<Outcome, Error> {
|
||||
let blob = serde_json::to_vec("routed-ok").unwrap();
|
||||
let blob_id = services.run.run_store.write_blob(&blob).await.unwrap();
|
||||
let blob_hash = services.run.run_store.write_blob(&blob).await.unwrap();
|
||||
let mut outcome = Outcome::success();
|
||||
outcome.context_updates.insert(
|
||||
context::keys::COMMAND_OUTPUT.to_string(),
|
||||
serde_json::json!(format_blob_ref(&blob_id)),
|
||||
serde_json::json!(format_blob_ref(&blob_hash)),
|
||||
);
|
||||
Ok(outcome)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,8 +217,8 @@ mod tests {
|
|||
};
|
||||
handle.append_run_event(&event).await.unwrap();
|
||||
|
||||
let blob_id = handle.write_blob(br#"{"ok":true}"#).await.unwrap();
|
||||
let blob = handle.read_blob(&blob_id).await.unwrap().unwrap();
|
||||
let blob_hash = handle.write_blob(br#"{"ok":true}"#).await.unwrap();
|
||||
let blob = handle.read_blob(&blob_hash).await.unwrap().unwrap();
|
||||
let events = handle.list_events().await.unwrap();
|
||||
|
||||
assert_eq!(events.len(), 2);
|
||||
|
|
|
|||
|
|
@ -544,13 +544,13 @@ async fn daytona_pipeline_artifact_offload_and_sync() {
|
|||
.get("response.big_output")
|
||||
.expect("context should have response.big_output");
|
||||
let pointer_str = pointer_value.as_str().expect("pointer should be a string");
|
||||
let expected_blob_id = fabro_types::BlobHash::new(
|
||||
let expected_blob_hash = fabro_types::BlobHash::new(
|
||||
&serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024)))
|
||||
.expect("large value should serialize"),
|
||||
);
|
||||
assert_eq!(
|
||||
pointer_str,
|
||||
fabro_types::format_blob_ref(&expected_blob_id),
|
||||
fabro_types::format_blob_ref(&expected_blob_hash),
|
||||
"checkpoint should persist a blob ref"
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ fn resolve_checkpoint_text(
|
|||
let Some(current) = value.as_str() else {
|
||||
return Ok(value.to_string());
|
||||
};
|
||||
let Some(blob_id) = parse_blob_ref(current) else {
|
||||
let Some(blob_hash) = parse_blob_ref(current) else {
|
||||
return Ok(current.to_string());
|
||||
};
|
||||
|
||||
|
|
@ -272,7 +272,7 @@ fn resolve_checkpoint_text(
|
|||
};
|
||||
let run = runtime.block_on(store.open_run_reader(&run_id))?;
|
||||
let bytes = runtime
|
||||
.block_on(run.read_blob(&blob_id))?
|
||||
.block_on(run.read_blob(&blob_hash))?
|
||||
.ok_or("checkpoint blob should exist")?;
|
||||
Ok(serde_json::from_slice::<String>(&bytes)?)
|
||||
},
|
||||
|
|
@ -10059,13 +10059,13 @@ async fn large_context_values_are_offloaded_to_artifact_store() {
|
|||
.expect("context should have response.big_output");
|
||||
let pointer_str = pointer_value.as_str().expect("pointer should be a string");
|
||||
|
||||
let expected_blob_id = fabro_types::BlobHash::new(
|
||||
let expected_blob_hash = fabro_types::BlobHash::new(
|
||||
&serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024)))
|
||||
.expect("large value should serialize"),
|
||||
);
|
||||
assert_eq!(
|
||||
pointer_str,
|
||||
fabro_types::format_blob_ref(&expected_blob_id),
|
||||
fabro_types::format_blob_ref(&expected_blob_hash),
|
||||
"value should be a durable blob ref"
|
||||
);
|
||||
|
||||
|
|
@ -10258,13 +10258,13 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() {
|
|||
.get("response.big_output")
|
||||
.expect("context should have response.big_output");
|
||||
let pointer_str = pointer_value.as_str().expect("pointer should be a string");
|
||||
let expected_blob_id = fabro_types::BlobHash::new(
|
||||
let expected_blob_hash = fabro_types::BlobHash::new(
|
||||
&serde_json::to_vec(&serde_json::json!("x".repeat(150 * 1024)))
|
||||
.expect("large value should serialize"),
|
||||
);
|
||||
assert_eq!(
|
||||
pointer_str,
|
||||
fabro_types::format_blob_ref(&expected_blob_id),
|
||||
fabro_types::format_blob_ref(&expected_blob_hash),
|
||||
"checkpoint should persist a blob ref"
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1841,18 +1841,22 @@ impl Client {
|
|||
.await?;
|
||||
response
|
||||
.into_inner()
|
||||
.id
|
||||
.hash
|
||||
.parse()
|
||||
.context("write_run_blob returned invalid blob id")
|
||||
.context("write_run_blob returned invalid blob hash")
|
||||
}
|
||||
|
||||
pub async fn read_run_blob(&self, run_id: &RunId, blob_id: &BlobHash) -> Result<Option<Bytes>> {
|
||||
pub async fn read_run_blob(
|
||||
&self,
|
||||
run_id: &RunId,
|
||||
blob_hash: &BlobHash,
|
||||
) -> Result<Option<Bytes>> {
|
||||
let response = self
|
||||
.current_state()
|
||||
.client
|
||||
.read_run_blob()
|
||||
.id(run_id.to_string())
|
||||
.blob_id(blob_id.to_string())
|
||||
.blob_hash(blob_hash.to_string())
|
||||
.send()
|
||||
.await;
|
||||
match response {
|
||||
|
|
|
|||
|
|
@ -1957,11 +1957,11 @@ pub fn json_snapshot_filters(mut filters: Vec<(String, String)>) -> Vec<(String,
|
|||
filters = json_elapsed_ms_snapshot_filters(filters);
|
||||
filters.push((
|
||||
r#""manifest_blob":\s*"[0-9a-f]{64}""#.to_string(),
|
||||
r#""manifest_blob": "[BLOB_ID]""#.to_string(),
|
||||
r#""manifest_blob": "[BLOB_HASH]""#.to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r#""definition_blob":\s*"[0-9a-f]{64}""#.to_string(),
|
||||
r#""definition_blob": "[BLOB_ID]""#.to_string(),
|
||||
r#""definition_blob": "[BLOB_HASH]""#.to_string(),
|
||||
));
|
||||
filters.push((
|
||||
r#""run_dir":\s*"\[STORAGE_DIR\]/scratch/\d{8}-\[ULID\]""#.to_string(),
|
||||
|
|
@ -2562,8 +2562,8 @@ mod tests {
|
|||
"inference_time_ms": "[INFERENCE_TIME_MS]",
|
||||
"tool_time_ms": "[TOOL_TIME_MS]",
|
||||
"active_time_ms": "[ACTIVE_TIME_MS]",
|
||||
"manifest_blob": "[BLOB_ID]",
|
||||
"definition_blob": "[BLOB_ID]",
|
||||
"manifest_blob": "[BLOB_HASH]",
|
||||
"definition_blob": "[BLOB_HASH]",
|
||||
"run_dir": "[RUN_DIR]",
|
||||
"message": "[CUSTOM]"
|
||||
}"#
|
||||
|
|
|
|||
|
|
@ -63,10 +63,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn conversion_preserves_digest_and_display() {
|
||||
let blob_id = BlobHash::new(b"workflow");
|
||||
let version_id = WorkflowVersionId::from(blob_id);
|
||||
assert_eq!(version_id.to_string(), blob_id.to_string());
|
||||
assert_eq!(BlobHash::from(version_id), blob_id);
|
||||
let blob_hash = BlobHash::new(b"workflow");
|
||||
let version_id = WorkflowVersionId::from(blob_hash);
|
||||
assert_eq!(version_id.to_string(), blob_hash.to_string());
|
||||
assert_eq!(BlobHash::from(version_id), blob_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -781,21 +781,21 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
|
|||
};
|
||||
},
|
||||
/**
|
||||
* Reads a previously stored blob by identifier.
|
||||
* Reads a previously stored blob by hash.
|
||||
* @summary Read Run Blob
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} blobId Content-addressed blob identifier.
|
||||
* @param {string} blobHash Content-addressed blob hash.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
readRunBlob: async (id: string, blobId: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
readRunBlob: async (id: string, blobHash: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('readRunBlob', 'id', id)
|
||||
// verify required parameter 'blobId' is not null or undefined
|
||||
assertParamExists('readRunBlob', 'blobId', blobId)
|
||||
const localVarPath = `/api/v1/runs/{id}/blobs/{blobId}`
|
||||
// verify required parameter 'blobHash' is not null or undefined
|
||||
assertParamExists('readRunBlob', 'blobHash', blobHash)
|
||||
const localVarPath = `/api/v1/runs/{id}/blobs/{blobHash}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
|
||||
.replace(`{${"blobId"}}`, encodeURIComponent(String(blobId)));
|
||||
.replace(`{${"blobHash"}}`, encodeURIComponent(String(blobHash)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
|
|
@ -905,7 +905,7 @@ export const RunInternalsApiAxiosParamCreator = function (configuration?: Config
|
|||
};
|
||||
},
|
||||
/**
|
||||
* Writes an opaque binary blob and returns its content-addressed blob identifier.
|
||||
* Writes an opaque binary blob and returns its content-addressed blob hash.
|
||||
* @summary Write Run Blob
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {File} body
|
||||
|
|
@ -1179,15 +1179,15 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
|
|||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Reads a previously stored blob by identifier.
|
||||
* Reads a previously stored blob by hash.
|
||||
* @summary Read Run Blob
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} blobId Content-addressed blob identifier.
|
||||
* @param {string} blobHash Content-addressed blob hash.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async readRunBlob(id: string, blobId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.readRunBlob(id, blobId, options);
|
||||
async readRunBlob(id: string, blobHash: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.readRunBlob(id, blobHash, options);
|
||||
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
|
||||
const localVarOperationServerBasePath = operationServerMap['RunInternalsApi.readRunBlob']?.[localVarOperationServerIndex]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
|
|
@ -1219,7 +1219,7 @@ export const RunInternalsApiFp = function(configuration?: Configuration) {
|
|||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
* Writes an opaque binary blob and returns its content-addressed blob identifier.
|
||||
* Writes an opaque binary blob and returns its content-addressed blob hash.
|
||||
* @summary Write Run Blob
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {File} body
|
||||
|
|
@ -1417,15 +1417,15 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
|
|||
return localVarFp.putStageArtifact(id, stageId, retry, body, filename, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Reads a previously stored blob by identifier.
|
||||
* Reads a previously stored blob by hash.
|
||||
* @summary Read Run Blob
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} blobId Content-addressed blob identifier.
|
||||
* @param {string} blobHash Content-addressed blob hash.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
readRunBlob(id: string, blobId: string, options?: RawAxiosRequestConfig): AxiosPromise<File> {
|
||||
return localVarFp.readRunBlob(id, blobId, options).then((request) => request(axios, basePath));
|
||||
readRunBlob(id: string, blobHash: string, options?: RawAxiosRequestConfig): AxiosPromise<File> {
|
||||
return localVarFp.readRunBlob(id, blobHash, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Returns the latest checkpoint data for a run, or null if no checkpoint has been recorded yet.
|
||||
|
|
@ -1448,7 +1448,7 @@ export const RunInternalsApiFactory = function (configuration?: Configuration, b
|
|||
return localVarFp.retrieveRunSettings(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Writes an opaque binary blob and returns its content-addressed blob identifier.
|
||||
* Writes an opaque binary blob and returns its content-addressed blob hash.
|
||||
* @summary Write Run Blob
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {File} body
|
||||
|
|
@ -1656,15 +1656,15 @@ export class RunInternalsApi extends BaseAPI {
|
|||
}
|
||||
|
||||
/**
|
||||
* Reads a previously stored blob by identifier.
|
||||
* Reads a previously stored blob by hash.
|
||||
* @summary Read Run Blob
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {string} blobId Content-addressed blob identifier.
|
||||
* @param {string} blobHash Content-addressed blob hash.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
public readRunBlob(id: string, blobId: string, options?: RawAxiosRequestConfig) {
|
||||
return RunInternalsApiFp(this.configuration).readRunBlob(id, blobId, options).then((request) => request(this.axios, this.basePath));
|
||||
public readRunBlob(id: string, blobHash: string, options?: RawAxiosRequestConfig) {
|
||||
return RunInternalsApiFp(this.configuration).readRunBlob(id, blobHash, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1690,7 +1690,7 @@ export class RunInternalsApi extends BaseAPI {
|
|||
}
|
||||
|
||||
/**
|
||||
* Writes an opaque binary blob and returns its content-addressed blob identifier.
|
||||
* Writes an opaque binary blob and returns its content-addressed blob hash.
|
||||
* @summary Write Run Blob
|
||||
* @param {string} id Unique run identifier (ULID).
|
||||
* @param {File} body
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@
|
|||
|
||||
|
||||
/**
|
||||
* Content-addressed identifier for a stored blob.
|
||||
* Content-addressed hash of a stored blob.
|
||||
*/
|
||||
export interface WriteBlobResponse {
|
||||
/**
|
||||
* Blob identifier.
|
||||
* Content-addressed hash of the stored blob.
|
||||
*/
|
||||
'id': string;
|
||||
'hash': string;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue