Unify blob hash vocabulary

This commit is contained in:
Scott Werner 2026-08-14 11:34:12 -04:00
parent 4e48d2887e
commit bf4265e1b8
30 changed files with 194 additions and 188 deletions

View file

@ -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) |

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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?;

View file

@ -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)
}

View file

@ -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
}

View file

@ -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]"

View file

@ -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))
}
}

View file

@ -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(

View file

@ -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) => {

View file

@ -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),

View file

@ -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(),
))

View file

@ -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)))
})
})

View file

@ -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();

View file

@ -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));
}

View file

@ -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)))
);
}

View file

@ -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))
}

View file

@ -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>> {

View file

@ -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)

View file

@ -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()))

View file

@ -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)
}

View file

@ -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);

View file

@ -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"
);

View file

@ -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"
);

View file

@ -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 {

View file

@ -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]"
}"#

View file

@ -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]

View file

@ -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

View file

@ -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;
}