Skip to content

[2/4] feat(api): E2B build-context upload endpoints and configuration - #72

Closed
JoyboyBrian wants to merge 2 commits into
kvcache-ai:mainfrom
JoyboyBrian:e2b-build/04-upload-api
Closed

[2/4] feat(api): E2B build-context upload endpoints and configuration#72
JoyboyBrian wants to merge 2 commits into
kvcache-ai:mainfrom
JoyboyBrian:e2b-build/04-upload-api

Conversation

@JoyboyBrian

@JoyboyBrian JoyboyBrian commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What

Expose the E2B build-context upload contract:

  • GET /templates/{templateID}/files/{hash} returns 201 with {present, url};
  • the returned bearer-token URL accepts a streaming PUT;
  • [template_build] adds upload, expansion, aggregate-context, TTL, timeout, and public-base-URL settings;
  • the HTTP server is regenerated from the OpenAPI source.

This is layer 2 of the active COPY/ADD stack, replayed onto current main at 00ba6cb without #70.

Layer PR Branch Scope
1/4 #71 e2b-build/03-build-file-store Build-context archive store
2/4 #72 e2b-build/04-upload-api Upload API and configuration
3/4 #73 e2b-build/05-copy-plan Host-side COPY planning
4/4 #74 e2b-build/06-copy-exec COPY/ADD execution, E2E, and documentation

Why

The official E2B SDK asks the template service whether a content-addressed context archive is present, uploads it when needed, and then references its hash in the build request. This layer implements that handshake on top of #71's repository store.

Related: #28. The original PR remains the full concept/reference discussion.

Scope and non-goals

This layer adds the OpenAPI contract, generated server surface, request handlers, streaming upload path, configuration, and focused tests. It does not plan guest paths or execute COPY/ADD. Existing-alias rebuild remains unsupported.

Exact layer diff: e2b-build/03-build-file-store...e2b-build/04-upload-api

Design

  • Existing content returns present: true; missing content receives a short-lived, single-use upload URL.
  • Uploads stream to the repository store and are bounded by configured size and timeout limits.
  • Hashes and bearer grants are validated before import.
  • template_build.public_base_url supports TLS termination and multi-hop deployments where the request Host is not the client-facing origin.
  • Generated code is derived from src/api/openapi.yml.

Compatibility and operations

  • Adds the E2B-compatible files endpoint; existing endpoints are unchanged.
  • Configuration defaults preserve direct-node and bundled-gateway behavior.
  • TLS-terminated or externally routed deployments should set template_build.public_base_url so the SDK receives the correct HTTPS upload origin.
  • No snapshot manifest change or new host service is required.

Validation

The rewritten stack was validated on Linux against the #74 tip:

cargo fmt --all -- --check
cargo test --locked -p agentenv --lib template::
# 68 passed, 0 failed
cargo clippy --locked -p agentenv --lib -- -D warnings
make agentenv-server
# regeneration produced no intended generated-code diff

This layer includes handler/config/store integration tests for present/missing archives, grant validation, upload bounds, timeouts, and public URL construction. GitHub CI validates this cumulative branch independently.

Reviewer notes

The security-sensitive surfaces are upload limits, grant single-use semantics, URL construction, and bearer-token handling. #70 is not required to merge this stack.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 13 issue(s) in this PR.

  • ✅ Successfully posted inline: 13 comment(s)

Comment on lines +74 to +85
if self
.client
.exists(&key)
.await
.map_err(|error| RepositoryError::backend("check build archive", error))?
{
return Ok(());
}
self.client
.put_file(&key, staged)
.await
.map_err(|error| RepositoryError::backend("upload build archive", error))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
This check-then-write does not provide the TemplateBuildFileStore::import first-write-wins guarantee. Two imports can both observe a missing key and then issue unconditional put_file operations, so the later completion replaces the first archive; a build can consequently materialize different bytes for the same hash. This is also reachable through concurrent replay because OSS grant claiming is non-atomic. Publish through a backend primitive that rejects overwrite, coordinate creation through an atomic shared lock/metadata store, or verify the content key so all racing writers are guaranteed to upload identical bytes.

Comment on lines +116 to +120
self.client
.put_bytes(&key, grant)
.await
.map_err(|error| RepositoryError::backend("write upload grant", error))?;
Ok(token)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[performance · medium]
Expired or abandoned grants are never removed by this implementation; only successfully claimed grants are deleted. Unless every deployment independently installs the referenced bucket lifecycle rule, repeated upload-link requests leave durable JSON objects indefinitely. Add backend cleanup/lifecycle provisioning (or make the required policy an explicitly validated deployment prerequisite) so expiry bounds retained grants as well as authorization.

Comment on lines +153 to +167
let Some(grant) = self.read_grant(&key).await? else {
return Ok(false);
};
if !grant.authorizes(template_id, hash, expires_unix, now_unix) {
return Ok(false);
}
// Consume the grant so the upload URL cannot be replayed. S3-compatible
// stores offer no conditional delete, so simultaneous replays of one
// token can both observe the grant; archives are immutable, which is
// what keeps that from mattering.
self.client
.delete(&key)
.await
.map_err(|error| RepositoryError::backend("consume upload grant", error))?;
Ok(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
The read-authorize-delete sequence does not actually consume the bearer grant once: concurrent requests can both read it, and object-store DELETE is normally idempotent, so both deletes can succeed and both calls return true. The stated safety argument does not hold because import above is also an exists-then-unconditional-write race, allowing replayed uploads to replace one another. Use an atomic claim primitive in a shared coordination store (or otherwise ensure only one request can transition the grant to claimed) before reporting success.

Comment on lines 186 to 188
if existing != record.id && self.snapshot_exists(&existing).await? {
return Err(RepositoryError::AliasConflict {
alias: alias.to_string(),
existing,
new_id: record.id.clone(),
});
bind_on_create = false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
When this branch is taken, the new record is still written with the same alias even though the binding remains on existing. If the rebuild fails, mark_build_error preserves that alias, and list() exposes records directly, so the old template and failed rebuild can advertise the same name indefinitely. Store an unbound/pending alias separately, or make listing derive the visible name from the alias catalog and expose the requested rebuild name through distinct build metadata.

Comment on lines +322 to +330
let record = self
.write_committed_record(
metadata.id.clone(),
metadata.alias.clone(),
metadata.resources,
committed,
metadata.source.clone(),
)
.await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
This durable write occurs before the alias move and has no cancellation/crash recovery. Cancellation, process termination, or an unrecoverable failure before bind_alias permanently leaves a committed record claiming an alias that still resolves to the previous snapshot. list() filters raw records, so clients can observe duplicate/misleading template names. Add reconciliation that derives alias ownership from alias bindings, or use a staged record state that listings do not expose until the alias move completes.

Comment on lines +762 to +766
if current.as_ref() != Some(id) {
return;
}

let key = match validated_alias_key(alias) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
This check and the subsequent unconditional restore are a lost-update race. After this read observes id, another publisher can bind the alias successfully, and this rollback then overwrites or deletes that newer binding. Because OSS offers no CAS here, rollback must use the same external per-alias serialization as binding; otherwise it is safer not to mutate the alias than to clobber a successful concurrent publish.

Comment on lines +801 to +805
if let Some(mut previous) = self.read_record(id).await? {
let claims_moved_alias = previous
.alias
.as_ref()
.is_some_and(|alias| alias.as_ref() == moved_alias);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
This is an unguarded read-modify-write of the entire record. A concurrent build-state update or deletion between read_record and write_record can be overwritten (and a deletion can even be resurrected). The cleanup also fails best-effort, leaving duplicate alias claims in listings after publish returns success. Serialize updates per snapshot/alias or introduce versioned conditional record updates; if consistent listing metadata is part of successful publication, propagate cleanup failure instead of warning only.

Comment on lines +118 to +121
if scanned >= MAX_PRUNE_SCAN {
break;
}
scanned += 1;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[performance · medium]
This always scans only the first 256 matching entries returned by read_dir. Directory iteration is commonly stable, so a prefix of fresh entries can permanently hide expired entries later in the directory; successive calls do not necessarily drain the backlog and disk usage can grow without bound. Persist/rotate a scan cursor, shard the directory and rotate shards, or otherwise ensure every entry is eventually visited.

Comment on lines +252 to +254
if final_path.exists() {
return Ok(());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
The caller-supplied hash is trusted without validating the staged bytes, and this global first-write-wins cache is shared across templates. An authenticated caller can request a known hash for its own template, upload arbitrary content first, and make other builds consume that content because subsequent imports return here. Verify the archive against the expected content identifier before publication, or namespace cache entries by the authorization/tenant boundary if this SDK hash is not a digest of the uploaded archive.

// Copy into the store filesystem first (the staged file usually
// lives on node-local tmp), then link it into place within the
// store directory so readers only ever observe complete archives.
let store_staged = root.join(format!(".import-{}.tmp", uuid::Uuid::new_v4()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
Interrupted imports can leave full-size .import-*.tmp files permanently. Normal error paths remove them, but process termination after copy/sync bypasses cleanup, and prune_expired only scans .tar files. Include stale import temporaries in opportunistic/startup pruning (using a sufficiently conservative age) so repeated crashes cannot exhaust repository storage.

Comment on lines +309 to +312
Ok(_) => {
Self::touch(&path);
Ok(Some(path))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
Returning a pathname after metadata/touch does not protect the archive from concurrent pruning. A pruner can observe the old mtime and unlink it after this check (or unlink between the check and touch), leaving the caller with a path that no longer exists. Materialize a hard link/copy into scratch_dir before returning, or introduce a lease/reference mechanism that pruning honors.

// committed record whose `alias` field names an alias that still
// resolves to the previous snapshot, so readers of `record.alias`
// (listings) may observe the stale claim until the next rebind.
store.write_json(&alias_path, &snapshot_id)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[other · high]
This unconditional rebind allows multiple rebuilds for the same alias to race. create leaves the alias on the current snapshot and does not reserve it for a particular pending build, so every pending build can reach this point; whichever build commits last wins, even if it was an older/stale rebuild that started before a newer successful one. Add an alias generation/reservation token (or otherwise serialize builds per alias) and verify it under this lock before replacing the binding.

Comment on lines +171 to +176
Some(existing)
if existing != record.id
&& store.load_record_by_id_unlocked(&existing)?.is_some() =>
{
Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
Leaving the binding untouched is correct for lookup availability, but the newly written record still contains the same alias. list() filters and returns record.alias directly without validating it against the authoritative alias file, so during every rebuild both the live snapshot and the pending build are exposed as owners of the alias. The documented commit crash window and best-effort cleanup can make that duplication permanent. Reconcile aliases while listing (clear/suppress an alias unless its binding points to that record), or persist pending/rebinding intent separately from the visible alias field.

Comment on lines +41 to +42
now_unix <= expires_unix
&& self.expires_unix == expires_unix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · low]
This accepts the credential at the exact expiration timestamp. Expiry instants are normally exclusive (now < expires), and the POSIX pruning code similarly retains a grant while expires_unix == now_unix. This leaves a one-second boundary window and can disagree with clients or gateways using standard expiration semantics. Use < consistently and add a boundary test for now_unix == expires_unix.

Suggestion:

Suggested change
now_unix <= expires_unix
&& self.expires_unix == expires_unix
now_unix < expires_unix
&& self.expires_unix == expires_unix

Comment on lines +72 to +75
/// authenticity: importing a hash that is already stored keeps the stored
/// archive, so an in-flight build can never observe its build context
/// change underneath it.
async fn import(&self, hash: &str, staged: &Path) -> RepositoryResult<()>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
The shared cache key is caller-controlled, but this contract explicitly does not verify that the uploaded archive corresponds to it. The upload-link handler accepts any syntactically valid hash, and both backends store archives globally as {hash}.tar; therefore, a caller can pre-populate another build's hash with arbitrary bytes. First-write-wins then makes the poisoned archive persistent and prevents the legitimate context from replacing it. Verify the archive using the SDK's hash algorithm before publishing, or scope keys/grants by the authenticated tenant/template so an untrusted key cannot affect other builds.

Comment on lines +101 to +104
/// Verification never removes the grant, so a request that fails before
/// the archive is stored can be retried with the same upload URL. Callers
/// must `claim_upload_grant` after publishing the archive, so a failed
/// publication leaves the URL retryable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
Requiring claim only after publication creates a check-then-act authorization race. The upload handler verifies, imports, and then claims; concurrent requests with the same token can both verify and race to first-write-wins import, so the request that loses the claim may still supply the permanently stored bytes. This violates the single-use guarantee even on POSIX where claiming itself is atomic. Claim/reserve the token after staging but before publishing (requiring a fresh link if publication fails), or introduce an atomic reservation state that can be released on failure and is required to commit the archive.

@JoyboyBrian
JoyboyBrian force-pushed the e2b-build/04-upload-api branch from d9943dc to d11d80a Compare August 7, 2026 04:59
@JoyboyBrian JoyboyBrian changed the title [4/6] feat(api): E2B build-context upload endpoints and [template_build] config [2/4] feat(api): E2B build-context upload endpoints and configuration Aug 7, 2026
Comment thread src/api/build_files.rs
Comment on lines +101 to +121
let authorized = match store
.verify_upload_grant(&query.token, &template_id, &hash, query.expires, now_unix)
.await
{
Ok(authorized) => authorized,
Err(error) => {
warn!(error = %error, "failed to verify build-file upload grant");
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to validate upload grant",
);
}
};
if !authorized {
return error_response(
StatusCode::UNAUTHORIZED,
"upload grant is invalid, expired, or already used; request a fresh upload link",
);
}

let max_bytes = ConfigManager::global_config()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
Verification is non-consuming and there is no per-grant reservation, global concurrency limit, or request-body admission control visible here. A holder of one valid URL can start many simultaneous uploads; each request allocates a staging file and accepts up to max_bytes, and then may perform an import. This permits disk, bandwidth, backend, and task exhaustion before the first request claims the token. Add a bounded concurrency/reservation mechanism before staging, and release it on failed or timed-out uploads so legitimate retries remain possible.

Comment thread src/api/build_files.rs
Comment on lines +226 to +237
if let Err(error) = store.import(&hash, &staged_path).await {
warn!(error = %error, hash, "failed to import build archive");
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"failed to store build archive; the upload can be retried with the same link",
);
}

// The archive is published, so the claim only enforces single-use: the
// atomic remove/delete picks a single winner among concurrent replays, and
// a replay that loses the race is rejected even though the archive it
// uploaded is stored. `now_unix` is the timestamp taken before the body was

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
The grant is only consumed after import, so concurrent requests using the same valid bearer URL can all pass verification and publish their bodies before any claim wins. Because hash is explicitly not checked against the uploaded bytes and OSS/Posix imports use first-write-wins, a replay that later returns 401 can still determine the archive stored for that hash, allowing an attacker who can replay the URL to poison the build context. Reserve/claim the grant before publishing (with a retry-safe reservation/state), or otherwise make publication conditional on the single successful claim and verify the received content against the hash.

Comment on lines +4203 to +4206
let claims = None.or(claims_in_header).or(claims_in_auth_header);
let Some(claims) = claims else {
return response_with_status_code_only(StatusCode::UNAUTHORIZED);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
This makes the new artifact-upload endpoint accept X-Team-ID as an independent credential and prefer it over bearer authentication. The configured extractor currently returns claims for any non-empty X-Team-ID (and the endpoint implementation does not use claims to verify ownership of template_id), so a caller can request an upload grant for another team's template and populate a caller-chosen hash. Require a validated API key/bearer/admin credential and enforce that the authenticated team owns the template before issuing the grant; tenant context should not itself authenticate the request.

Comment on lines +4209 to +4213
let validation = tokio::task::spawn_blocking(move || {
templates_template_id_files_hash_get_validation(path_params)
})
.await
.unwrap();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
A JoinError from this blocking task (for example, if validation panics or the task is cancelled during runtime shutdown) currently panics the request handler rather than returning the endpoint's server-error response. The same .await.unwrap() pattern is also used in each newly added serialization branch. Propagate/log JoinError as StatusCode::INTERNAL_SERVER_ERROR instead of unwrapping it; ideally fix the generator/template so regeneration preserves the change.

Suggestion:

Suggested change
let validation = tokio::task::spawn_blocking(move || {
templates_template_id_files_hash_get_validation(path_params)
})
.await
.unwrap();
let validation = tokio::task::spawn_blocking(move || {
templates_template_id_files_hash_get_validation(path_params)
})
.await
.map_err(|e| {
error!(error = ?e, "path validation task failed");
StatusCode::INTERNAL_SERVER_ERROR
})?;

Comment thread src/api/impls/template.rs
Comment on lines +398 to +401
let base = config
.public_base_url
.clone()
.unwrap_or_else(|| format!("http://{host}"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
Do not derive this credential-bearing upload URL from the untrusted request Host header. No Host allowlist or trusted-origin validation is applied before this handler, and public_base_url is optional by default. If a client reaches the API through an attacker-controlled host/proxy (or any layer permits Host spoofing), the response directs the SDK's unauthenticated PUT to that host, disclosing both the bearer token and potentially sensitive build-context contents. Require a configured trusted public_base_url, or validate the Host against an explicit allowlist and derive the scheme/origin from trusted server configuration.

Suggestion:

Suggested change
let base = config
.public_base_url
.clone()
.unwrap_or_else(|| format!("http://{host}"));
let Some(base) = config.public_base_url.as_deref() else {
return Ok(
TemplatesTemplateIdFilesHashGetResponse::Status500_ServerError(Self::error(
500,
"template_build.public_base_url must be configured for build-context uploads",
)),
);
};

Comment on lines +74 to +85
if self
.client
.exists(&key)
.await
.map_err(|error| RepositoryError::backend("check build archive", error))?
{
return Ok(());
}
self.client
.put_file(&key, staged)
.await
.map_err(|error| RepositoryError::backend("upload build archive", error))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
This check-then-put does not implement the trait's required first-write-wins immutability. Two imports for a previously absent hash can both observe false, and the later unconditional put_file overwrites the object selected by the earlier upload. A build that materializes between or after those writes can therefore see different bytes for the same hash. Please publish with an atomic no-overwrite primitive, or add backend-wide coordination/indirection that selects one immutable upload without subsequently replacing it.

Comment on lines +112 to +120
let mut scanned: usize = 0;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_none_or(|ext| ext != extension) {
continue;
}
if scanned >= MAX_PRUNE_SCAN {
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
This bounded scan can permanently starve entries beyond the first 256. read_dir commonly returns a stable order, so every invocation may inspect the same fresh files while older/expired files later in the directory are never reached; there is no other POSIX cleanup path in this change. Persist a cursor, randomize/rotate traversal, shard entries into bounded directories, or run an occasional complete background sweep so repeated calls actually drain the backlog.

Comment on lines +309 to +312
Ok(_) => {
Self::touch(&path);
Ok(Some(path))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
Returning the shared cache path after touching it does not protect it from concurrent pruning. A prune can read the old mtime immediately before this touch, then unlink the archive after materialize returns but before the caller opens it. Copy/link the archive into the provided caller-owned scratch_dir (or otherwise pin active entries) before returning so eviction cannot invalidate a successfully materialized result.

/// authenticity: importing a hash that is already stored keeps the stored
/// archive, so an in-flight build can never observe its build context
/// change underneath it.
async fn import(&self, hash: &str, staged: &Path) -> RepositoryResult<()>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
The archive key is scoped only by hash, while import explicitly accepts a caller-supplied key without verifying the uploaded bytes against it. A grant is bound to template_id, but that binding is discarded at publication time, so an upload authorized for one template can permanently populate a hash later requested by another template (or poison a hash that another build expects). Include the template identity in the archive key, or verify the staged archive's canonical digest before publishing and ensure the cache semantics are safe for cross-template reuse.

Suggestion:

Suggested change
async fn import(&self, hash: &str, staged: &Path) -> RepositoryResult<()>;
async fn import(
&self,
template_id: &str,
hash: &str,
staged: &Path,
) -> RepositoryResult<()>;

Comment on lines +101 to +104
/// Verification never removes the grant, so a request that fails before
/// the archive is stored can be retried with the same upload URL. Callers
/// must `claim_upload_grant` after publishing the archive, so a failed
/// publication leaves the URL retryable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security · high]
This contract requires verification, publication, and grant consumption as separate operations, and explicitly tells callers to publish before claiming. Two concurrent requests can both pass verify_upload_grant, then either request can publish its bytes before the atomic claim selects a winner; the losing claim does not undo the already-published archive. Thus single-use grants do not prevent a replay from determining the immutable first-write-wins content. Make authorization/claim and publication one atomic backend operation (or reserve the grant before publication with a failure-safe rollback), and only expose success after the winning publication is established.

Suggestion:

Suggested change
/// Verification never removes the grant, so a request that fails before
/// the archive is stored can be retried with the same upload URL. Callers
/// must `claim_upload_grant` after publishing the archive, so a failed
/// publication leaves the URL retryable.
/// Implementations must provide an atomic authorize-and-publish operation so
/// /// concurrent requests cannot publish before one grant claimant wins.

@JoyboyBrian

Copy link
Copy Markdown
Contributor Author

Closing as superseded by the direction agreed in #141: build context streams directly into the build VM via envd in the aenv-orchestrated native workflow and is never persisted in the snapshot repository or on the host filesystem, so the durable archive store and the E2B upload endpoints have no landing spot. Focused design: #147.

@JoyboyBrian JoyboyBrian closed this Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant